概念:
-
所谓排序,就是把一堆杂乱的数据,排成升序或降序(递增/增减)。 -
排序稳定性: 假设一组数据[1,2,9,5,5,6,8],进行升序排序后,两个5的相应位置不发生改变,即称为稳定的排序,否则就是不稳定排序。 -
内部排序: 数据元素全部放在内存中进行排序。 -
外部排序: 即将待排序的记录存储在外存中,排序时再把数据一部分一部分地调入内存进行排序,在排序过程中需要多次进行内存和外存之间地交换。
1. 插入排序
思路:
- 默认为第一个元素自己是有序的,从第二个元素开始。
- 取出第二个元素tmp,往前进行比较。
- 若该元素比tmp大,则将该元素往后移一位,直到找到比tmp小的。
- 找到比tmp小于等于的元素后,tmp插入到该元素的下一位。
- 循环2~4步骤。
步骤具体实现:
- 定义下标 i ,遍历数组。默认 i 下标已经是有序的,把i下标元素存入tmp。
- 定义 j,j 从 i-1 的位置,开始向前遍历,遇到比 tmp大的元素,就把此时j下标的元素往后移一位,直到下标等于或小于0停止。
- j下标元素小于tmp,则把tmp元素插入该j下标的下一个位置。
时间复杂度: O(N^2);
空间复杂度: O(1);
稳定性: 稳定;
具体代码实现:
public static void insertSort(int[] array){
for (int i = 0; i < array.length; i++) {
int tmp = array[i];
int j = i-1;
for ( ;j >=0 ; j--) {
if(array[j] > tmp){
int exchange = array[j];
array[j+1] = array[j];
array[j] = exchange;
}else{
break;
}
}
array[j+1] = tmp;
}
}
结论:
- 当数据趋于有序时,排序时间越快,最好的情况下时间复杂度为O(N);
- 当把循环中
array[j] > tmp 的大于号改为大于等于,此时就不是稳定的排序了。
2. 希尔排序
思路:
- 先将待排序列进行预排序,使得待排序列接近有序,此时进行插入排序。
- 把待排序的数据分为多个组,每组间隔为5或3…。
- 若此组的第一个元素大于最后一个元素,将此组第一个元素和最后一个元素交换。
- 重复上述操作,直到每组间隔只有1时,所有数据都在统一组内进行排好序。
步骤具体实现:
- 定义gap = 数组长度\2。
- 把待排序列分为gap个组,每个组的第一个元素和最后一个元素进行比较交换。
- 重复上述操作
- 当gap为1时,进行插入排序。
时间复杂度: O(N^1.3);
空间复杂度: O(1);
稳定性: 不稳定。
具体代码实现:
public static void shell(int[] array){
int gap = array.length;
while(gap > 1){
gap /= 2;
for (int i = 0; i < array.length; i++) {
int tmp = array[i];
int j = i-gap;
for ( ;j >=0 ; j-=gap) {
if(array[j] > tmp){
int exchange = array[j];
array[j+gap] = array[j];
array[j] = exchange;
}else{
break;
}
}
array[j+gap] = tmp;
}
}
}
结论:
-
希尔排序是对插入排序的优化。 -
当gap>1时,都是预排序,目的是为了让数组更趋于有序,当gap==1时,数组已经接近有序,这样进行插入排序就会很快。 -
希尔排序的时间复杂度不容易计算,因为gap的取值方法很多,导致很难计算,因此我们按照Knuth提出的时间复杂度O(N^1.3)来算。
3. 选择排序
思路:
每次从待排序列中选择一个最小值(最大),存放在序列的起始位置,直到全部待排序的数据排完。
步骤具体实现:
-
定义i ,假设待排序列i 下标元素是最小值,用min记录当前i 下标。 -
定义j,从i 下一个位置开始往后遍历,遇到小于array[min] 时更新min,min指向每次遍历的最小值下标,直到遍历完一次数组。 -
一次遍历完后array[i] 和min下标进行交换。 -
重复上述操作,直到待排序数据剩余1个元素。
时间复杂度: O(N^2);
空间复杂度: O(1);
稳定性: 不稳定;
具体代码实现:
public static void selectSort(int[] array){
for (int i = 0; i < array.length; i++) {
int min = i;
for (int j = i+1; j < array.length; j++) {
if(array[j] < array[min]){
min = j;
}
}
if(min != i){
int tmp = array[i];
array[i] = array[min];
array[min] = tmp;
}
}
}
结论: 效率不是很好,实际中很少使用。
4. 堆排序
注意:排升序需要建大根堆,排降序建小根堆。 此文章默认举例排升序。
思路&具体步骤实现:
-
首先得建立一个大根堆。 -
把根节点与最后一个节点交换,每一次交换,最后一个节点向前走一步。 -
进行堆向下调整。
时间复杂度: O(N*logN);
空间复杂度: O(1);
稳定性: 不稳定;
具体代码实现:
public static void heapSort(int[] array){
createHeap(array);
int end = array.length-1;
while (end > 0){
int tmp = array[0];
array[0] = array[end];
array[end] = tmp;
shiftDown(array,0,end);
end--;
}
}
private static void createHeap(int[] array){
for (int parent = (array.length-1-1)/2; parent >=0 ; parent--) {
shiftDown(array,parent,array.length);
}
}
private static void shiftDown(int[] array,int parent,int len){
int child = (parent*2)+1;
while(child < len){
if(child+1 < len && array[child+1] > array[child]){
child++;
}
if(array[child] > array[parent]){
int tmp = array[child];
array[child] = array[parent];
array[parent] = tmp;
parent = child;
child = (parent*2)+1;
}else {
break;
}
}
}
5. 冒泡排序
思路: 根据序列中的两个记录键值比较结果来交换这两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾部移动,键值较小的向前部移动。
具体步骤实现:
- 定义
i 遍历数组,控制趟数,总体趟数比数组长度少1; - 每趟让一个较大值移动到尾部。
- 定义
j 每次从0下标进行两两比较交换。
时间复杂度: O(N^2);
空间复杂度: O(1);
稳定性: 稳定;
具体代码实现:此处冒泡排序代码作了优化。
public static void bubbleSort(int[] array){
for (int i = 0; i < array.length-1; i++) {
boolean flag = false;
for (int j = 0; j < array.length-1-i; j++) {
if (array[j] > array[j + 1]) {
int tmp = array[j];
array[j] = array[j + 1];
array[j + 1] = tmp;
flag = true;
}
}
if(flag == false){
break;
}
}
}
6.快速排序
基本思想:任取待排序元素中的某元素作为基准值,将待排序集合分割为两子序,左子序中所有元素均小于 基准值,右子序均大于 基准值,然后左右子序重复该过程,直到所有元素都排列在相应位置上。
6.1 Hoare版
思路:
- 取数组最左边或最右边为key。
- 先从右边开始找到小于key值。
- 再从左边走找到大于key值,左右进行交换。
- 左右相遇后,相遇点为此次遍历的基准值
- 最后与key位置交换。
具体实现步骤:
-
定义key,指向数组最左边的元素。 -
定义left从数组左边开始遍历;定义right从右边开始遍历。 -
一定要right先走,再走left。 -
当right和left相遇后,与key交换,相遇点为基准值。 -
遍历以该基准值分割的两子序。 -
递归重复上述操作。
具体代码实现:
public static void quickSort(int[] array){
quickHelp(array,0,array.length-1);
}
private static void quickHelp(int[] array,int start,int end){
if (start >= end){
return;
}
int pivot = hoare(array,start,end);
quickHelp(array,start,pivot-1);
quickHelp(array,pivot+1,end);
}
private static int hoare(int[] array,int left ,int right){
int index = left;
int key = array[left];
while(left < right){
while(left < right && array[right] >= key){
right--;
}
while(left < right && array[left] <= key){
left++;
}
int tmp = array[left];
array[left] = array[right];
array[right] = tmp;
}
int tmp = array[left];
array[left] = array[index];
array[index] = tmp;
return left;
}
代码优化:
此排序,递归下去就好像一颗二叉树,当排序的是有序时,就只有左子树或者只有右子树,此时排序的时间复杂度最高,所有:在找基准值先,尽量让左右子树划分平衡。当递归到一定层数是进行插入排序,因为越往下层遍历,这一层的递归次数越多,比如第一层递归2次,第二次递归4次…。
步骤:
-
设定一个边界值,达到边界值时进行插入排序。 -
保证每次划分均匀:(3个数找中数) 如待排序:1 2 3 4 5 把1和3和5进行比较,找出中间值,把最左边值和中间值交换。
具体代码实现:
public static void quickSort(int[] array){
quickHelp(array,0,array.length-1);
}
private static void quickHelp(int[] array,int start,int end){
if (start >= end){
return;
}
if(start <= 15){
insertSortHelp(array,start,end);
}
int index = findMin(array,start,end);
swap(array,start,index);
int pivot = hoare(array,start,end);
quickHelp(array,start,pivot-1);
quickHelp(array,pivot+1,end);
}
public static void insertSortHelp(int[] array,int left,int right){
for (int i = left; i <= right; i++) {
int tmp = array[i];
int j = i-1;
for ( ;j >=0 ; j--) {
if(array[j] > tmp){
swap(array,j,j+1);
}else{
break;
}
}
array[j+1] = tmp;
}
}
private static int findMin(int[] array,int left,int right){
int midIndex = (left+right)/2;
if(array[left] < array[right]){
if(array[left] > array[midIndex]){
return left;
}else if(array[right] < array[midIndex]){
return right;
}else {
return midIndex;
}
}else{
if(array[left] < array[midIndex]){
return left;
}else if(array[right] > array[midIndex]){
return right;
}else {
return midIndex;
}
}
}
private static int hoare(int[] array,int left ,int right){
int index = left;
int key = array[left];
while(left < right){
while(left < right && array[right] >= key){
right--;
}
while(left < right && array[left] <= key){
left++;
}
swap(array,left,right);
}
swap(array,left,index);
return left;
}
private static void swap(int[] array,int i,int j) {
int tmp = array[i];
array[i] = array[j];
array[j] = tmp;
}
6.2 挖坑法
思路:
大致思路根Hoare版本类似;
- 选最左边的元素存入key,在此位置形成一个坑位;
- 先从右开始遍历和key比较,再从左遍历,进行左右交换。
- 左右相交后,把key的放入相交点位置。
具体实现步骤:
- 先将第一个元素存入临时变量key,形成一个坑位。
- 定义left从数组左边开始走、right从右边开始走。
- 先从右开始遍历找到比key小的元素,放入left的位置;
- 然后left找到比key大的元素,放入right的位置。
- 待left和right下标相遇后,把key的元素放入相交点。
- 重复上述操作。
具体代码实现【递归】:
public static void digSort(int[] array,int start ,int end){
if(start >= end){
return;
}
int left = start;
int right = end;
int key = array[left];
while(left < right){
while (left < right && array[right] >= key){
right--;
}
array[left] = array[right];
while (left < right && array[left] <= key){
left++;
}
array[right] = array[left];
}
array[left] = key;
digSort(array,start,left-1);
digSort(array,left+1,end);
}
6.3 前后指针法
了解即可,见得比较少,整体思路大体一样。
- 定义key存储数组起始元素,prev指向数组开始位置;cur指prev后一个位置。
- 当cur下标元素小于key,prev向后走,并且此时prev下标的元素不等于cur下标元素,则进行交换
- 否则cur一直往后走,当cur走完时,prev下标和数组最左边左边下标交换。
- 递归重复上述操作。
public static void quick(int[] array,int start,int end){
if(start >= end){
return;
}
int pivot = partition(array,start,end);
quick(array,start,pivot-1);
quick(array,pivot+1,end);
}
private static int partition(int[] array, int left, int right) {
int prev = left ;
int cur = left+1;
while (cur <= right) {
if(array[cur] < array[left] && array[++prev] != array[left]) {
swap(array,cur,prev);
}
cur++;
}
swap(array,prev,left);
return prev;
}
6.4 快速排序非递归
思路:使用栈,模拟递归
- 先找到基准
- 判断基准左右是否存在两个元素及以上。
- 把以基准为界左边的数组最左边下标和最右边下标入栈
- 再基准右边的数组最左边下标和最右边下标入栈
- 弹出栈顶两个下标,对此下标区间再进行找基准【弹出顺序:先右后左】
- 重复上述操作。
- 快速排序非递归最重要的就是找基准。
具体代码实现:
public static void quickSort2(int[] array){
Stack<Integer> stack = new Stack<>();
int left = 0;
int right = array.length-1;
int pivot = finMid(array,left,right);
if(pivot > left+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(array,left,right);
if(pivot > left+1){
stack.push(left);
stack.push(pivot-1);
}
if(pivot < right-1){
stack.push(pivot+1);
stack.push(right);
}
}
}
private static int finMid(int[] array,int left ,int right){
int index = left;
int key = array[left];
while(left < right){
while(left < right && array[right] >= key){
right--;
}
while(left < right && array[left] <= key){
left++;
}
swap(array,left,right);
}
swap(array,left,index);
return left;
}
快速排序总结:
- 综合性能和使用场景比较好。
- 一般快速排序三种方法使用顺序:挖坑法->Hoare法->前后指针法
7. 归并排序
核心思想:分而治之
即:将待排序列拆分,再合并成为有序序列。
- 当拆分到只有一个元素时,再从下往上逐层合并,首先对第一层序列号1(元素4),和序列号2(元素5)进行合并
1.创建一个大数组,长度为序列号1和序列号2长度之和,s1、s2指针分别指向两个小序列号(数组)的第一个元素,ret指向大数组的第一个元素。【两个有序数组合并为一个数组】
2.比较[s1]、[s2]指向的元素,4<5,将4放入ret指向的空间,ret往右走一步,s1往右走一步。
3.此时序列号1(数组)已经没有元素,直接将序列号2的元素放入大数组中。
4.[8]和[1]、[7]和[2]、[6]和[3],用同样方式进行合并。
5.以[4,5]为序列1,[1,8]为序列2,继续进行合并。
6.[4]和[1]比较,4>1,把[1]放入ret指向的空间,[s2]往右走一步。
7.重复上述操作,直到把[4,5]和[1,8]合并成【1,4,5,8】。
以[2,7]为序列3,[3,6]为序列4,用同样的方式合并成为新的序列【2,3,6,7】;
8.最后将[1,4,5,8]和[2,3,6,7]用同样的方式合并成新的序列
时间复杂度:O(N*logN) : 归并排序算法每次将序列折半分组,共需要logN轮。
空间复杂度:O(N): 归并排序算法排序过程中需要额外的一个序列去存储排序后的结果,所占空间是N。
稳定性:稳定
具体代码实现:
public static void merge(int[] array,int start,int end){
if(start >= end){
return;
}
int mid = (start+end)/2;
merge(array,start,mid);
merge(array,mid+1,end);
mergeHelp(array,start,mid,end);
}
private static void mergeHelp(int[] array,int left,int mid,int right){
int s1 = left;
int e1 = mid;
int s2 = mid+1;
int e2 = right;
int[] ret = new int[right-left+1];
int k = 0;
while(s1 <= e1 && s2 <= e2){
if( array[s1] <= array[s2]){
ret[k++] = array[s1++];
} else{
ret[k++] = array[s2++];
}
}
while(s1 <= e1){
ret[k++] = array[s1++];
}
while(s2 <= e2){
ret[k++] = array[s2++];
}
for (int i = 0; i < k; i++) {
array[i+left] = ret[i];
}
}
非递归版本:【模拟递归】
public static void mergerSort(int[] array){
int gap = 1;
while(gap < array.length){
for (int i = 0; i < array.length; i+= 2*gap) {
int left = i;
int mid = left+gap-1;
if(mid >= array.length){
mid = array.length-1;
}
int right = mid+gap;
if(right >= array.length){
right = array.length-1;
}
mergeHelp(array,left,mid,right);
}
gap *= 2;
}
}
private static void mergeHelp(int[] array,int left,int mid,int right){
int s1 = left;
int e1 = mid;
int s2 = mid+1;
int e2 = right;
int[] ret = new int[right-left+1];
int k = 0;
while(s1 <= e1 && s2 <= e2){
if( array[s1] <= array[s2]){
ret[k++] = array[s1++];
} else{
ret[k++] = array[s2++];
}
}
while(s1 <= e1){
ret[k++] = array[s1++];
}
while(s2 <= e2){
ret[k++] = array[s2++];
}
for (int i = 0; i < k; i++) {
array[i+left] = ret[i];
}
}
总结:
- 归并的缺点是需要O(N)的空间复杂度。
- 归并排序的思想更多是在解决磁盘中的外排序问题。
8. 排序算法复杂度及稳定性分析
排序方法 | 最好 | 平均 | 最坏 | 空间复杂度 | 稳定性 |
---|
冒泡排序 | 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(logn)~O(n) | 不稳定 | 归并排序 | O(n * log(n)) | O(n * log(n)) | O(n * log(n)) | O(n) | 稳定 |
|