排序常见的可以分为三大类
- 冒泡排序、选择排序
- 插入排序、希尔排序
- 归并排序、快速排序
因为排序中涉及到很多的元素比较、元素交换位置、元素打印等,所以方便起见,先封装一个工具类,如下所示:
public class Utils {
/**
* 打印数组
*/
public static void printArray(int[] array) {
for (int i = 0; i < array.length; i++) {
System.out.print(array[i] + " ");
}
System.out.println();
}
/**
* 交换数组 下标a与下标b的位置
*/
public static void swap(int[] array, int a, int b) {
int temp = array[a];
array[a] = array[b];
array[b] = temp;
}
/**
* 返回 位置a的元素 是否 小于位置b的元素
*/
public static boolean less(int[] array, int a, int b) {
return array[a] < array[b];
}
}
冒泡排序
思路
- 排序过程,两两比较,i指针每一趟都会确定一个最大值
- 因为总会确定一个位置,那么j指针的右区间也在逐渐缩小
- i指针每一趟总是把最大值排好,那么j指针右区间逐渐变小,左区间肯定都是从0开始。因为右边排好了,左边还没排
public void sort(int[] nums) {
for (int i = 0; i < nums.length - 1; i++) {
for (int j = 0; j < nums.length - 1 - i; j++) {
if (Utils.less(nums, j + 1, j)) {
Utils.swap(nums, j + 1, j);
}
}
}
Utils.printArray(nums);
}
比如输入 5,4,3,2,1 会经过下面一系列转换过程
5,4,3,2,1 |
---|
4 3 2 1 5 |
3 2 1 4 5 |
2 1 3 4 5 |
1 2 3 4 5 |
选择排序
思路
- 排序过程,i指针每一趟都会选择一个最小值,然后跟当前i交互元素
- 与冒泡排序不同的是,选择排序在比较过程中,不着急交换位置,而是j遍历完了,再把最小值与当前i的交换元素的值
public void sort(int[] nums) {
for (int i = 0; i < nums.length - 1; i++) {
int minIndex = i;
for (int j = i + 1; j < nums.length; j++) {
if (Utils.less(nums, j, minIndex)) {
minIndex = j;
}
}
if (minIndex != i) {
Utils.swap(nums, i, minIndex);
}
}
Utils.printArray(nums);
}
比如输入 5,4,3,2,1 会经过下面一系列转换过程
5,4,3,2,1 |
---|
1,4,3,2,5 |
1,2,3,4,5 |
1,2,3,4,5 |
1,2,3,4,5 |
总结一下,选择排序是对冒泡排序的改进,排序思路都很相似,n条数据的话都需要n-1趟,每一趟呢都把最值给安排好,放在相应位置。不同的是,冒泡排序在比较的过程中需要两两比较,交换位置,以达到目的。而选择排序是先记录更新最新小值的下标,第i趟结束之后才选择是否交换。还需要注意的是,==冒泡排序实际上是先把最大值排好,所以右边是有序的左边无序,所以j每次要从0开始;而选择排序则相反,每次是选出最小值,放在左边,所以左边右序而右边无序,所以j要从i+1开始。==
插入排序
思路
- 插入排序适用于,大部分数据是有序的了,通过插入排序使得最终有序
- i指针向右 j指针向左,插入排序不会访问索引右侧的元素
public void sort(int[] nums) {
for (int i = 1; i < nums.length; i++) {
for (int j = i; j > 0; j--) {
if (Utils.less(nums, j, j - 1)) {
Utils.swap(nums, j, j - 1);
}
}
}
Utils.printArray(nums);
}
归并排序
思路
- 归并排序使用的算法思想是分治法
- 两个过程,先分 再合并
- 分的过程使用递归,合并过程借助一个辅助数组,将两个数组合并到辅助数组中去
/**
* 归并排序
* 两个过程
* 1.先分 再 2.合并
*/
public void sort(int[] nums) {
Utils.printArray(sort(nums, 0, nums.length - 1));
}
public int[] sort(int[] nums, int lo, int hi) {
if (lo == hi) {
return new int[]{nums[lo]};
} else {
int middle = (lo + hi) / 2;
return merge(sort(nums, lo, middle), sort(nums, middle + 1, hi));
}
}
private int[] merge(int[] a, int[] b) {
int[] result = new int[a.length + b.length];
int i = 0;
int j = 0;
int k = 0;
while (i < a.length && j < b.length) {
if (a[i] < b[j]) {
result[k] = a[i];
i++;
} else {
result[k] = b[j];
j++;
}
k++;
}
if (i < a.length) {
while (i < a.length) {
result[k++] = a[i++];
}
}
if (j < b.length) {
while (j < b.length) {
result[k++] = a[j++];
}
}
return result;
}
这种写法是在分的过程中就返回数组,这样在合并的过程中,直接合并俩数组就好。另外一种写法在分的过程中不返回数组,在合并的时候开辟多个辅助数组来解决,代码如下:
/**
* 归并排序
* 两个过程
* 1.先分 再 2.合并
*/
public void sort(int[] nums) {
Utils.printArray(nums);
sort(nums, 0, nums.length - 1);
Utils.printArray(nums);
}
public void sort(int[] nums, int lo, int hi) {
if (lo >= hi) {
return;
}
int middle = (lo + hi) / 2;
sort(nums, lo, middle);
sort(nums, middle + 1, hi);
merge(nums, lo, middle, hi);
}
private void merge(int[] nums, int lo, int mid, int hi) {
int[] copyA = new int[mid - lo + 1];
int[] copyB = new int[hi - mid];
int[] auxArray = new int[hi - lo + 1]; // 辅助数组,最后再复制到原数组nums中
for (int i = lo; i <= mid; i++) {
copyA[i - lo] = nums[i];
}
for (int i = mid + 1; i <= hi; i++) {
copyB[i - mid - 1] = nums[i];
}
int i = 0;
int j = 0;
int k = 0;
while (i < copyA.length && j < copyB.length) {
if (copyA[i] < copyB[j]) {
auxArray[k] = copyB[i];
i++;
} else {
auxArray[k] = copyB[j];
j++;
}
k++;
}
if (i < copyA.length) {
while (i < copyA.length) {
auxArray[k++] = copyA[i++];
}
}
if (j < copyB.length) {
while (j < copyB.length) {
auxArray[k++] = copyB[j++];
}
}
for (int l = 0; l < auxArray.length; l++) {
nums[lo + l] = auxArray[l];
}
}
快速排序
思路
- 快速排序首先要选取一个标定点作为参考,比它小的放在左边,比它大的放在右边
- 主要是一个切分的过程partition函数,partition方法执行步骤为:假如选取的是第一个元素,那么j指针就要先向左走;反之,如果选取的是最后一个元素,那么i指针就要先向右走
- partition过程,如图所示:
第一步 j开始向左走
j遇到第一个比标定点6小的数,停止脚步;i开始向右走,找到了第一个比标定点大的元素,也停止脚步
这时候 i 与 j交换元素,交换完成之后,j继续向左边走,重复上面步骤
再次交换位置
交换完成之后,继续j向右
i== j,两者相遇
标定点的位置与j或者i交换一下
完成这次切分过程,返回j或者i,就是partition的切分点
代码实现如下:
public void sort(int[] nums) {
Utils.printArray(nums);
sort(nums, 0, nums.length - 1);
Utils.printArray(nums);
}
private void sort(int[] nums, int lo, int hi) {
if (lo >= hi) return;
int p = partition(nums, lo, hi);
sort(nums, lo, p);
sort(nums, p + 1, hi);
}
private int partition(int[] nums, int lo, int hi) {
int v = nums[lo]; // 标定点,假如就取第一位作为参考
int i = lo;
int j = hi;
while (i < j) {
while (i < j && nums[j] >= v) {
j--;
}
// while循环结束,表示右侧大的元素都走完了,剩下是比标定点小的,等待交换元素
while (i < j && nums[i] <= v) {
i++;
}
// while循环结束,表示左侧小的元素都走完了,剩下是比标定点大的,等待交换元素
if (i < j) {
Utils.swap(nums, i, j);
}
// i大的元素与j小元素交换
}
Utils.swap(nums, lo, j); // 最后执行完了,标定点的位置需要跟j或者i(j==i)交换
return j;
}
// 输 入 2 1 3 5 4
// 排序后 1 2 3 4 5
总结一下快速排序与归并排序,二者都是分治法思想,时间复杂度是O(nlogn),极端情况会退化成O(n2)。归并排序比较耗费空间,因为归并两个数组的过程,需要开辟一个辅助数组。快速排序比较不稳定,理想情况是选取一个数作为参考,每次都能一分为二,假如这个参考选择的不好,那么每次不能分为两块,就会退化成O(n2)