目录
1. 直接插入排序
2. 希尔排序
3. 选择排序
4. 堆排序
5. 冒泡排序
6. ⭐快速排序
6.1 递归法
6.2 非递归法
7. ⭐归并排序
7.1 递归法
7.2 非递归法
8. 海量数据的排序问题
9. 总结
每次选择无序区间的第一个元素,在有序区间选择合适的位置插入。
有一组数组:3 44 38 5 47 19
当 i = 0 时,temp = 3;因为 j < 0, array[j + 1] = temp, array[0] = 3;整体后数组为 {3}
当 i = 1 时,j = 0,temp = 44; 因为 j > 0, array[j] < temp, j--, array[1] = temp = 44;整体后数组为 {3,44}
当 i = 2 时,j = 1,temp = 38;因为 j > 0, array[j] > temp, array[2] = 44, j--, array[1] = 38;整体后数组为 {3,38,44}
当 i = 3 时,j = 2,temp = 5;因为 j > 0, array[j] > temp, array[3] = 44, j--, array[1] > temp, array[2] = 38, j--, array[1] = 5; 整体后数组为 {3,5,38,44}
....
i 往后一位,将 i 下标的数值存到 temp 中,j = i - 1 下标中的数与 temp 中的数值相比较,如果 j 下标的数大,就与 i 下标中的数交换位置;如果 j 下标中的数小,就将 temp 中的数放回到 i 下标。如果 j 减为负的,将 temp 中的数放到下标为0的位置中。
代码实现:
public static void insertSort(int[] array) {
for (int i = 0; i < array.length; i++) {
int temp = array[i];
int j = i - 1;
for (; j >= 0; j--) {
if (array[j] > temp) {
array[j + 1] = array[j];
} else {
break;
}
}
// j回退到了小于0的情况
array[j + 1] = temp;
}
}
时间复杂度:o(n^2) 最好的时间复杂度:o(n)
对于直接插入排序来说,数据越有序,排序速度越快。
空间复杂度:o(1)
稳定性:稳定的
希尔排序法又称缩小增量法。希尔排序法的基本思想是:先选定一个整数,把待排序文件中所有记录分成个组,所有距离为的记录分在同一组内,并对每一组内的记录进行排序。然后重复上述分组和排序的工作。当到达分组为1时,所有记录在统一组内排好序。
1. 希尔排序是对直接插入排序的优化。
2. 当gap > 1时都是预排序,目的是让数组更接近于有序。当gap == 1时,数组已经接近有序的了,这样就会很快。这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试的对比。
第一趟先将数组分成5组,如上图9和4,1和8,2和6,5和3,7和5,两两比较,如果前一个数大于后一个数交换位置;
第二趟将数组分为2组,如上图4,2,5,8,5是一组,2,3,9,6,7是一组,进行排序,组内排序就和直接插入排序一样,选择无序中的第一个元素,在有序区间选择合适位置插入。
第三趟将数组分为1组,如上图,进行排序。
代码实现:
public static void shellSort(int[] array) {
int gap = array.length;
while (gap > 1) {
shell(array, gap);
gap /= 2;
}
shell(array, 1);
}
}
public static void shell(int[] array, int gap) {
for (int i = gap; i < array.length; i++) {
int temp = array[i];
int j = i - gap;
for (; j >= 0; j -= gap) {
if (array[j] > temp) {
array[j + gap] = array[j];
} else {
break;
}
}
array[j + gap] = temp;
}
}
时间复杂度【和增量有关系】:o(n^1.3 - n^1.5)
空间复杂度:o(1)
稳定性:不稳定的,看在比较的过程当中 是否发生了跳跃式的交换,如果发生了跳跃式的交换,那么就是不稳定的排序。
每一次从无序区间选出相邻两个元素,比较两者大小,大的存放在无序区间的最后(或最前),直到全部待排序的数据元素排完 。
代码实现:
public static void selectSort(int[] array) {
for (int i = 0; i < array.length; i++) {
for (int j = i + 1; j < array.length; j++) {
swap(array, i, j);
}
}
}
public static void swap(int[] array, int i, int j) {
if (array[j] < array[i]) {
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
代码优化:每一次从无序区间选出最大(或最小)的一个元素,存放在无序区间的最后(或最前),直到全部待排序的数据元素排完 。
代码实现:
public static void selectSort2(int[] array) {
for (int i = 0; i < array.length; i++) {
int minIndex = i;
for (int j = i + 1; j < array.length; j++) {
if (array[j] < array[minIndex]) {
minIndex = j;
}
}
swap(array, i, minIndex);
}
}
public static void swap(int[] array, int i, int j) {
if (array[j] < array[i]) {
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
时间复杂度:o(n^2)
空间复杂度:o(1)
稳定性:不稳定
基本原理也是选择排序,只是不在使用遍历的方式查找无序区间的最大的数,而是通过堆来选择无序区间的最大的数。排升序要建大堆;排降序要建小堆。
大顶堆:每个节点的值都大于或者等于它的左右子节点的值。
对于大顶堆:arr[i] >= arr[2i + 1] && arr[i] >= arr[2i + 2]
小顶堆:每个节点的值都小于或者等于它的左右子节点的值。
对于小顶堆:arr[i] <= arr[2i + 1] && arr[i] <= arr[2i + 2]
第一步建大根堆,首先将现在的无序序列看成一个堆结构,一个没有规则的二叉树,将序列里的值按照从上往下,从左到右依次填充到二叉树中。
然后根据大顶堆的性质,每个节点的值都大于或等于它的左右子树节点值。需要找到所有包含子节点的节点,也就是非叶子节点,调整父子关系,即也是从最后 1 个非叶子结点开始,也就是109开始进行从左到右从下到上进行调整。至于如何知道最后一个非叶子节点的位置,从上往下,从左往右填充二叉树的过程中,第一个叶子节点,一定是序列长度/2,所以第一个非叶子节点的索引就是arr.length / 2 -1。找到最后一个非叶子节点后,比较它的左右孩子节点值,如果孩子值比他大就换位置,971 比 109 大,交换位置。
然后每一层一次调整位置,直到该堆成为一个大根堆。
第二步将堆顶元素和尾部元素位置交换,使末尾元素最大,然后调整剩下的元素构建大根堆,使堆顶元素成为第二大元素。如此反复进行交换、重构、交换。这样所有元素就达到了有序状态。
代码实现:
public static void heapSort(int[] arr) {
// 1.建堆o(n)
createHeap(arr);
int end = arr.length - 1;
// 2.交换然后调整o(n * log n)
while (end > 0) {
swap(arr, 0, end);
shiftDown(arr, 0, end);
end--;
}
}
// 建大根堆
private static void createHeap(int[] arr) {
for (int parent = (arr.length - 1 - 1) / 2; parent >= 0; parent--) {
shiftDown(arr, parent, arr.length);
}
}
private static void shiftDown(int[] arr, int parent, int length) {
int child = 2 * parent + 1;
while (child < length) {
if (child + 1 < length && arr[child] < arr[child + 1]) {
child++;
}
// child 下标就是左右孩子最大值的下标
if (arr[child] > arr[parent]) {
swap(arr, child, parent);
parent = child;
child = 2 * parent + 1;
} else {
break;
}
}
}
时间复杂度:o(n * log n)
空间复杂度:o(1)
稳定性:不稳定
在无序区间,通过相邻两个数的比较,将最大的数冒泡到无序区间的最后,持续这个过程,直到数组整体有序。
数组:3 44 38 5 47 19
第一趟:
3 和 44 比较,顺序不变;
44 和 38 比较,交换位置,{3,38,44}
44 和 5 比较,交换位置,{3,38,5,44}
44 和 47 比较,顺序不表,{3,38,5,44,47}
47 和 19 比较,交换位置,{3,38,5,44,19,47}
第二趟:
{3,5,38,19,44,47}
第三趟:
{3,5,19,38,44,47}
每一趟走完都会有一个最大的数沉到最后。
代码实现:
public static void bubbleSort(int[] array) {
for (int i = 0; i < array.length - 1; i++) {
for (int j = 0; j < array.length - i - 1; j++) {
if (array[j] > array[j + 1]) {
swap(array, j, j + 1);
}
}
}
}
public static void swap(int[] array, int i, int j) {
if (array[j] < array[i]) {
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
}
时间复杂度:O(n^2)
空间复杂度:O(1)
稳定性:稳定的排序
代码优化:每次沉到最后的数字都是已经排好序的,下一次继续比较时还会进行比较,为了减少重复操作,可以添加标志位,如果已经比较过了,可以改变标志位,跳出循环。
代码实现:
public static void bubbleSort2(int[] array) {
for (int i = 0; i < array.length - 1; i++) {
boolean flag = false;
for (int j = 0; j < array.length - i - 1; j++) {
if (array[j] > array[j + 1]) {
swap(array, j, j + 1);
flag = true;
}
}
if (flag == false) {
break;
}
}
}
时间复杂度:o(n^2)
最好的情况下:有序o(n)
空间复杂度:o(1)
快速排序(Quick sort)是对冒泡排序的一种改进。快速排序由C. A. R. Hoare在1960年提出。
整体思路:选择一个基准值,通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比基准值小,另外一部分的所有数据都要比基准值大。
步骤:
1. 从待排序区间选择一个数,作为基准值(pivot);
2. Partition: 遍历整个待排序区间,将比基准值小的(可以包含相等的)放到基准值的左边,将比基准值大的(可以包含相等的)放到基准值的右边;
3. 采用分治思想,对左右两个小区间按照同样的方式处理,直到小区间的长度等于1,代表已经有序,或者小区间的长度等于0,代表没有数据。
代码实现:
挖坑法:将基准值取出来(挖坑)从右边走遇到小于基准值的元素将它填入坑中,然后右边就会有一个坑,当从左边遇到大于基准值的元素,将他填入右边的坑中,依次类推,直到左右边界相遇,结束递归。
public static void quickSort(int[] array, int left, int right) {
if (left >= right) {
return;
}
int pivot = partition(array,left,right);// 基准
quickSort(array,left,pivot - 1);// 分治
quickSort(array,pivot + 1, right);
}
private static int partition(int[] array, int start, int end) {
int temp = array[start];
while (start < end) {
// 先从右边走
while (start < end && array[end] >= temp) {
end--;
}
// end下标就遇到了 < temp 的值,交换位置,小于放在左边
array[start] = array[end];
while (start < end && array[start] <= temp) {
start++;
}
// start下标就遇到了 > temp 的值,交换位置,大于放在右边
array[end] = array[start];
}
array[start] = temp;
return start;
}
时间复杂度:
最好:O(k * n * logn) 每次可以均匀的分割待排序序列
最坏:数据有序或者逆序的情况O(n^2)
空间复杂度:
最好:O(logn)
最坏:O(n)
稳定性:不稳定
Hoare法:与挖坑法类似但是是遇到左边大于基准值的元素与右边小于基准值的元素直接交换位置。
public static void quickSort(int[] array, int left, int right) {
if (left >= right) {
return;
}
int pivot = partition2(array,left,right);// 基准
quickSort(array,left,pivot - 1);// 分治
quickSort(array,pivot + 1, right);
}
public static int partition2(int[] array, int left, int right) {
int i = left;
int j = right;
int pivot = array[left];
while(i < j) {
while (i < j && array[j] >= pivot) {
j--;
}
while (i < j && array[i] <= pivot) {
i++;
}
swap(array, i ,j);
}
swap(array,i,left);
return i;
}
上述排序算法重点在于基准的选择,对于基准有三种选择方式: 1. 选择数组的边上(左边或者右边)2. 随机选择 3. 几数取中(例如三数取中)其中几数取中法最佳,其他两种选择基准的方法可能会碰到单分支的树,降低效率。
所以对于上述代码可以选择几数取中法来进行优化:
优化一代码实现:
public static void quickSort3(int[] array, int left, int right) {
if (right <= left) {
return;
}
// 找基准之前,找到中间大小的值-使用三数取中法
int midValue = findMidValue(array,left,right);
swap(array,midValue,left);
int pivot = partition(array,left,right);
quick(array,left,pivot - 1);
quick(array,pivot + 1, right);
}
private static int findMidValue(int[] arr, int start, int end) {
int mid = start + ((end - start) >>> 1);
if(arr[start] < arr[end]) {
if(arr[mid] < arr[start]) {
return start;
} else if(arr[mid] > arr[end]) {
return end;
} else {
return mid;
}
} else {
if(arr[mid] > arr[start]) {
return start;
} else if(arr[mid] < arr[end]) {
return end;
} else {
return mid;
}
}
}
private static int partition(int[] array, int start, int end) {
int temp = array[start];
while (start < end) {
// 先从右边走
while (start < end && array[end] >= temp) {
end--;
}
// end下标就遇到了 < temp 的值,交换位置,小于放在左边
array[start] = array[end];
while (start < end && array[start] <= temp) {
start++;
}
// start下标就遇到了 > temp 的值,交换位置,大于放在右边
array[end] = array[start];
}
array[start] = temp;
return start;
}
除了选择基准优化代码,还可以利用直接插入排序越有序越快的特点来进行优化。
优化二代码实现:
public static void quickSort3(int[] array, int left, int right) {
if (right <= left) {
return;
}
// 如果区间内的数据在排序过程中小于某个范围了,可以使用直接插入排序
// 这个范围看使用者自己规定
if(right - left + 1 <= 400){
insertSort(array,left,right);
return;
}
// 找基准之前,找到中间大小的值-使用三数取中法
int midValue = findMidValue(array,left,right);
swap(array,midValue,left);
int pivot = partition(array,left,right);
quick(array,left,pivot - 1);
quick(array,pivot + 1, right);
}
private static int findMidValue(int[] arr, int start, int end) {
int mid = start + ((end - start) >>> 1);
if(arr[start] < arr[end]) {
if(arr[mid] < arr[start]) {
return start;
} else if(arr[mid] > arr[end]) {
return end;
} else {
return mid;
}
} else {
if(arr[mid] > arr[start]) {
return start;
} else if(arr[mid] < arr[end]) {
return end;
} else {
return mid;
}
}
}
public static void insertSort(int[] array, int start, int end) {
for (int i = 1; i <= end; i++) {
int temp = array[i];
int j = i - 1;
for (; j >= start; j--) {
if(array[j] > temp) {
array[j + 1] = array[j];
} else {
break;
}
}
array[j + 1] = temp;
}
}
private static int partition(int[] array, int start, int end) {
int temp = array[start];
while (start < end) {
// 先从右边走
while (start < end && array[end] >= temp) {
end--;
}
// end下标就遇到了 < temp 的值,交换位置,小于放在左边
array[start] = array[end];
while (start < end && array[start] <= temp) {
start++;
}
// start下标就遇到了 > temp 的值,交换位置,大于放在右边
array[end] = array[start];
}
array[start] = temp;
return start;
}
数组:{5,1,2,4,3,6,9,7,10,8}
先找到基准,基准是6,再将基准两边的边界的索引放到栈中,将下标0,4,6,9放入栈中;弹出栈顶元素,right = 9,left = 6,再次在这个区间找基准,基准为下标为8的9{5,1,2,4,3,6,8,7,9,10}。再将基准两边放入栈中,将下标6,7放入栈中,由于基准右边只剩一个元素所以右边的元素不用放在栈中。然后再次弹出栈顶元素,right = 7,left = 6,再次在这个区间找基准。后面的步骤类似,直到栈为空。
public static void quickSort4(int[] arr) {
Stack stack = new Stack<>();
int left = 0, right = arr.length - 1;
int pivot = partition(arr, left, right);
if (pivot > left + 1) {
// 基准左边有大于1个的元素
stack.push(left);
stack.push(pivot - 1);
}
if (pivot < right - 1) {
stack.push(pivot + 1);
stack.push(right);
}
while (!stack.isEmpty()) {
right = stack.pop();
left = stack.pop();
pivot = partition(arr, left, right);
if (pivot > left + 1) {
// 基准左边有大于1个的元素
stack.push(left);
stack.push(pivot - 1);
}
if (pivot < right - 1) {
stack.push(pivot + 1);
stack.push(right);
}
}
}
private static int partition(int[] array, int start, int end) {
int temp = array[start];
while (start < end) {
// 先从右边走
while (start < end && array[end] >= temp) {
end--;
}
// end下标就遇到了 < temp 的值,交换位置,小于放在左边
array[start] = array[end];
while (start < end && array[start] <= temp) {
start++;
}
// start下标就遇到了 > temp 的值,交换位置,大于放在右边
array[end] = array[start];
}
array[start] = temp;
return start;
}
优化总结:
1. 选择基准值很重要,通常使用几数取中法.
2. 待排序区间小于一个阈值时(例如 400),使用直接插入排序。3. 要分治处理左右两个区间。
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
所以学习归并排序前,可以先学习如何将两个有序数组合并成一个有序数组。
创建一个 ret 数组存储两个数组合并后的结果,将两个数组从第一个元素开始比较,谁小就先放入 ret 数组中,然后再用下一位进行比较,依次比较,如果有其中一个数组为空,就将另一个数组剩下的值都拷贝进 ret 数组中,直到两个数组都为空。
代码实现:
public static int[] mergeArray(int[] arr1, int[] arr2) {
int[] temp = new int[arr1.length + arr2.length];
int k = 0;
int start1 = 0;
int end1 = arr1.length - 1;
int start2 = 0;
int end2 = arr2.length - 1;
while (start1 <= end1 && start2 <= end2) {
if (arr1[start1] <= arr2[start2]) {
temp[k++] = arr1[start1++];
} else {
temp[k++] = arr2[start2++];
}
}
// 如果arr1中还有数据,将arr1中的数据拷贝到temp中
while (start1 <= end1) {
temp[k++] = arr1[start1++];
}
// 如果arr2中还有数据,将arr2中的数据拷贝到temp中
while (start2 <= end2) {
temp[k++] = arr2[start2++];
}
return temp;
}
懂得了如何合并两个有序数组,那么归并排序就也会了。 归并排序原理就是合并有序数组,如图:
代码实现:
public static void mergeSort(int[] array) {
mergeSortInternal(array, 0, array.length - 1);
}
// 使用分治法将数组分解成有序数组
public static void mergeSortInternal(int[] arr, int low, int high) {
if (low >= high) {
return;
}
int mid = low + ((high - low) >>> 1);
// 左边
mergeSortInternal(arr, low, mid);
// 右边
mergeSortInternal(arr, mid + 1, high);
// 合并
merge(arr, low, mid, high);
}
// 合并有序数组
private static void merge(int[] arr, int low, int mid, int high) {
int[] temp = new int[high - low + 1];
int k = 0;
int s1 = low;
int e1 = mid;
int s2 = mid + 1;
int e2 = high;
while (s1 <= e1 && s2 <= e2) {
if (arr[s1] <= arr[s2]) {
temp[k++] = arr[s1++];
} else {
temp[k++] = arr[s2++];
}
}
while (s1 <= e1) {
temp[k++] = arr[s1++];
}
while (s2 <= e2) {
temp[k++] = arr[s2++];
}
// 拷贝temp数组的元素,放入原来的数组arr中
for (int i = 0; i < k; i++) {
arr[i + low] = temp[i];
}
}
代码实现:
非递归法就是使数组从1个数据有序到2个数据有序.......一直到 array.length 个数据都有序,这样整个数据都是有序的。
public static void mergeSort2(int[] array) {
// 数组从1个数据有序到2个数据有序。。。一直到array.length个数据都有序。
int nums = 1;
while (nums < array.length) {
// 数组每次都要进行遍历,确定归并的区间
for (int i = 0; i < array.length; i += nums * 2) {
int left = i;
int mid = left + nums - 1;
if (mid >= array.length) {
mid = array.length - 1;
}
int right = mid + nums;
if (right >= array.length) {
right = array.length - 1;
}
// 下标确定之后,进行合并
merge(array, left, mid, right);
}
nums *= 2;
}
}
private static void merge(int[] arr, int low, int mid, int high) {
int[] temp = new int[high - low + 1];
int k = 0;
int s1 = low;
int e1 = mid;
int s2 = mid + 1;
int e2 = high;
while (s1 <= e1 && s2 <= e2) {
if (arr[s1] <= arr[s2]) {
temp[k++] = arr[s1++];
} else {
temp[k++] = arr[s2++];
}
}
while (s1 <= e1) {
temp[k++] = arr[s1++];
}
// 如果s2中还有数据,将s2中的数据拷贝到temp中
while (s2 <= e2) {
temp[k++] = arr[s2++];
}
// 拷贝temp数组的元素,放入原来的数组arr中
for (int i = 0; i < k; i++) {
arr[i + low] = temp[i];
}
}
内存只有 1G,需要排序的数据有 100G,应该如何排序?
因为内存中因为无法把所有数据全部放下,所以需要外部排序,而归并排序是最常用的外部排序。外部排序:排序过程需要在磁盘等外部存储进行的排序。
步骤:
1. 先把文件切分成 200 份,每个 512 M;
2. 分别对 512 M 排序,因为内存已经可以放的下,所以任意排序方式都可以;
3. 进行 200 路归并,同时对 200 份有序文件做归并排序,最终结果就有序了。
排序方法 |
最好 |
一般 |
最坏 |
空间复杂度 |
稳定性 |
直接插入排序 |
O(n) |
O(n^2) |
O(n^2) |
O(1) |
稳定 |
希尔排序 |
o(n^1.3 - n^1.5) |
O(1) |
不稳定 |
||
选择排序 |
o(n^2) |
o(n^2) |
o(n^2) |
O(1) |
不稳定 |
堆排序 |
o(n * log(n)) |
o(n * log(n)) |
o(n * log(n)) |
O(1) |
不稳定 |
冒泡排序 |
O(n) |
O(n^2) |
O(n^2) |
O(1) |
稳定 |
快速排序 |
o(k * n * log(n)) |
o(k * n * log(n)) |
O(n^2) |
o(log(n))~o(n) |
不稳定 |
归并排序 |
O(n * log(n)) |
O(n * log(n)) | O(n * log(n)) | O(n) |
稳定 |