【本节目标】
1、掌握七大基于比较的排序算法基本原理及实现
2、掌握排序算法的性能分析
3、掌握Java中常用的排序算法
目录
一、插入排序
1.1 直接插入排序
算法思想:直接插入排序是一种简单的插入排序法,其基本思想是把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列。实际中我们玩扑克牌时,就用了插入排序的思想。
排序原理:
1、从第一个元素开始,该元素可以认为已经有序
2、定义两个变量i和j,i从第二个元素开始遍历,一直遍历完数组,j每次从i-1的位置向前遍历
3、申请一个临时的遍历tmp,把i下标的值给tmp,如果j下标的值大于tmp,就把j下标的值给j+1下标的值,一直循环排序i前面的元素是否有序
代码实现:
- 时间复杂度分析:最坏情况下为O(N*2),此时待排序列为逆序,或者说接近逆序
最好情况下为O(N),此时待排序列为升序,或者说接近升序。
- 空间复杂度分析:只是开辟了一个 tmp 的变量 i,j,常数,即空间复杂度:O(1)
- 稳定性:稳定
public static void insertSort(int[] array) {
for (int i = 1; i < array.length; i++) {
int tmp = array[i];
int j = i - 1;
for(; j >= 0; j--) {
if(array[j] > tmp) {
array[j + 1] = array[j];
} else {
break;
}
}
array[j + 1] = tmp;
}
}
1.2 希尔排序(缩小增量排序)
代码实现:
- 时间复杂度分析:希尔排序的时间复杂度不好分析, 这里我们就大概记一下,约为O(N^1.3-N^1.5)
- 空间复杂度分析:仍然开辟的是常数个变量,空间复杂度为 O(1)
- 稳定性:不稳定
public static void shellSort(int[] array) { int gap = array.length; while (gap > 1) { gap /= 2; shell(array, gap); } //整体进行直接插入排序 shell(array, 1); } private static void shell(int[] array, int gap) { for (int i = gap; i < array.length; i++) { int tmp = array[i]; int j = i - gap; for (; j >= 0; j -= gap) { if(array[j] > tmp) { array[j + gap] = array[j]; } else { break; } } array[j + gap] = tmp; } }
二、选择排序
2.1 选择排序
基本思想:每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完 。
排序原理:
1、在元素集合 array[i]--array[n-1] 中选择关键码最大 ( 小 ) 的数据元素2、若它不是这组元素中的最后一个 ( 第一个 ) 元素,则将它与这组元素中的最后一个(第一个)元素交换3、在剩余的 array[i]--array[n-2] ( array[i+1]--array[n-1] )集合中,重复上述步骤,直到集合剩余 1 个元素
- 时间复杂度分析:O(N^2)
- 空间复杂度分析:仍然开辟的是常数个变量,空间复杂度为 O(1)
- 稳定性:不稳定
public static void selectSort(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);
}
}
private static void swap(int[] array, int i, int j) {
int tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}
下列代码是采用双下标的方法,一下可以排2个数,但本质上时间复杂度仍为O(N^2),没有任何变化。
public static void selectSort(int[] array) {
int left = 0;
int right = array.length - 1;
while (left < right) {
int minIndex = left;
int maxIndex = left;
for (int i = left + 1; i <= right; i++) {
if(array[i] < array[minIndex]) {
minIndex = i;
}
if(array[i] > array[maxIndex]) {
maxIndex = i;
}
}
if(maxIndex == left) {
maxIndex = minIndex;
}
swap(array, minIndex, left);
swap(array, maxIndex, right);
left++;
right--;
}
}
private static void swap(int[] array, int i, int j) {
int tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}
2.2 堆排序
代码实现:
时间复杂度分析:建堆的时间复杂度优先级队列那期有说过为 O(n),排序调整堆的时候,一共要调整 n-1 次,每次向下调整的时间复杂度是 logn,即 O(n*logn),加上面建堆的时间复杂度:O(n) + O(n*logn),最终时间复杂度也就是:O(n*logn)。
空间复杂度分析:O(1)
稳定性:不稳定
public static void heapSort(int[] array) {
createBigHeap(array);
int end = array.length - 1;
while (end > 0) {
swap(array, 0, end);
shfitDown(array, 0, end);
end--;
}
}
private static void createBigHeap(int[] array) {
for (int parent = (array.length - 1 - 1) / 2; parent >= 0; parent--) {
shfitDown(array, parent, array.length);
}
}
private static void shfitDown(int[] array, int parent, int len) {
int child = 2 * parent + 1;
while (child < len) {
if(child + 1 < len && array[child] < array[child + 1]) {
child++;
}
//child一定是左右节点最大值的下标
if(array[child] > array[parent]) {
swap(array, child, parent);
parent = child;
child = 2 * parent + 1;
} else {
break;
}
}
}
private static void swap(int[] array, int i, int j) {
int tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}
三、交换排序
3.1 冒泡排序
排序原理:
- 比较相邻的元素。如果前一个元素比后一个元素大,就交换这两个元素的位置。
- 对每一对相邻元素做同样的工作,从开始第一对元素到结尾的最后一对元素。最终最后位置的元素就是最大
- 每次比较得到的最大值下次就不用再比较了
代码实现:
- 时间复杂度分析:O(N^2)
- 空间复杂度分析:仍然开辟的是常数个变量,空间复杂度为 O(1)
- 稳定性:稳定
public static void bubbleSort(int[] array) {
for (int i = 0; i < array.length - 1; i++) {
boolean flg = true;
for (int j = 0; j < array.length - 1 - i; j++) {
if(array[j] > array[j + 1]) {
swap(array, j, j+1);
flg = false;
}
}
if(flg) {
break;
}
}
}
3.2 快速排序
快速排序是对冒泡排序的一种改进。它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,以此达到整个数据变成有序序列。
排序原理:
1、首先设定一个分界值,通过该分界值将数组分成左右两部分;
2、将大于或等于分界值的数据放到到数组右边,小于分界值的数据放到数组的左边。此时左边部分中各元素都小于或等于分界值,而右边部分中各元素都大于或等于分界值;
3、然后,左边和右边的数据可以独立排序。对于左侧的数组数据,又可以取一个分界值,将该部分数据分成左右两部分,同样在左边放置较小值,右边放置较大值。右侧的数组数据也可以做类似处理
4、重复上述过程,可以看出,这是一个递归定义。通过递归将左侧部分排好序后,再递归排好右侧部分的顺序。当左侧和右侧两个部分的数据排完序后,整个数组的排序也就完成了。
3.2.1 Hoare版
思路:
1、选出一个key,一般是最左边或是最右边的。
2、定义一个L和一个R,L从左向右走,R从右向左走。(需要注意的是:若选择最左边的数据作为key,则需要R先走;若选择最右边的数据作为key,则需要L先走)。
3、在走的过程中,若R遇到小于key的数,则停下,L开始走,直到L遇到一个大于key的数时,将L和R的内容交换,R再次开始走,如此进行下去,直到L和R最终相遇,此时将相遇点的内容与key交换即可。(选取最左边的值作为key)
4.此时key的左边都是小于key的数,key的右边都是大于key的数
5.将key的左序列和右序列再次进行这种单趟排序,如此反复操作下去,直到左右序列只有一个数据,或是左右序列不存在时,便停止操作,此时此部分已有序
上图是采用一次找基准,发现6左边的元素都比6小,右边的都比6大,然后下一次循环再依次进行递归,知道整体有序。
代码实现:
- 时间复杂度分析:需要递归树的高度,也就是logN,而每次递归都是O(n),所以时间复杂度为O(N*logN),但是如果每次递归都是单分支树,那么就是最坏的情况,O(N^2)
- 空间复杂度分析:每次递归都会压栈,随之开辟空间,那么快排类似于二叉树的前序遍历,左子树遍历完了,再有右子树,也就是会压栈,也会出栈,那么最大压栈多少呢?显然是树的高度,即 O(logn)。
- 稳定性:不稳定
public static void quickSort(int[] array) { quick(array, 0, array.length - 1); } private static void quick(int[] array, int left, int right) { if(left >= right) { return; } //三数取中间法 int index = midThree(array, left, right); swap(array, left, index); int pivot = partition2(array, left, right); quick(array, left, pivot - 1); quick(array, pivot + 1, right); } //三数取中法 private static int midThree(int[] array, int left, int right) { int mid = left + (right - left) >>> 2; if(array[left] < array[right]) { if(array[mid] < array[left]) { return left; } else if(array[mid] > array[right]) { return right; } else { return mid; } } else { if(array[mid] > array[left]) { return left; } else if(array[mid] < array[right]) { return right; } else { return mid; } } } private static int partition2(int[] array, int left, int right) { int i = left; int tmp = array[left]; while (left < right) { while (left < right && array[right] >= tmp) { right--; } while (left < right && array[left] <= tmp) { left++; } swap(array, left, right); } swap(array, left, i); return left; } private static void swap(int[] array, int i, int j) { int tmp = array[i]; array[i] = array[j]; array[j] = tmp; }
3.2.2 挖洞法
思路:
挖坑法思路与hoare版本思路类似
1.选出一个数据(一般是最左边或是最右边的)存放在key变量中,在该数据位置形成一个坑
2、还是定义一个L和一个R,L从左向右走,R从右向左走。(若在最左边挖坑,则需要R先走;若在最右边挖坑,则需要L先走)
过程:
(1)定义left、right并赋上相应的下标,记录当前left下标所对应的元素为tmp
(2)左边的left向右走、右边的right向左走。right先走,right找到比tmp小的值停下来,将right所在位置的元素赋给left所在的位置。left找到比tmp大的值停下来,将left所在位置的元素赋给right所在的位置。
(3)当left>=right时,将tmp的值赋给left所在的位置,返回left
(4)以left下标右边和左边为新的待排序数组继续执行1~3,直到整个数组完成排序。
代码实现:
private static int partition(int[] array, int left, int right) {
int tmp = array[left];
while (left < right) {
while (left < right && array[right] >= tmp) {
right--;
}
swap(array, left, right);
while (left < right && array[left] <= tmp) {
left++;
}
swap(array, left, right);
}
array[left] = tmp;
return left;
}
private static void swap(int[] array, int i, int j) {
int tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}
四、归并排序
代码实现:
- 时间复杂度:O(N*logN)
- 空间复杂度:最多会开辟数组长度个空间即 O(N)
- 稳定性:稳定
public static void mergeSort(int[] array) { mergeSortFunc(array, 0, array.length - 1); } private static void mergeSortFunc(int[] array, int left, int right) { if(left >= right) { return; } int mid = (left + right) / 2; mergeSortFunc(array, left, mid); mergeSortFunc(array, mid + 1, right); merge(array, mid, left, right); } private static void merge(int[] array, int mid, int left, int right) { int s1 = left; int s2 = mid + 1; int[] tmp = new int[right - left + 1]; int k = 0;//记录临时数组tmp的下标 while (s1 <= mid && s2 <= right) { if(array[s1] < array[s2]) { tmp[k++] = array[s1++]; } else { tmp[k++] = array[s2++]; } } //s1还没遍历完 while(s1 <= mid) { tmp[k++] = array[s1++]; } //s2还没遍历完 while(s2 <= right) { tmp[k++] = array[s2++]; } for (int i = 0; i < tmp.length; i++) { array[i + left] = tmp[i]; } }
排序方法 最好 平均 最坏 空间复杂度 稳定性 冒泡排序O(n)O(n^2)O(n^2)O(1)稳定插入排序O(n)O(n^2) O(n^2) O(1)稳定选择排序O(n^2) O(n^2) O(n^2) O(1)不稳定希尔排序O(n) O(n^1.3)O(n^2) O(1)不稳定堆排序O(n * log(n))O(n * log(n)) O(n * log(n)) O(1)不稳定快速排序O(n * log(n))O(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) 稳定