目录
常见的排序算法:
(以下排序方法都按从小到大升序排列为例)
一、直接插入排序
1、 实现思路:
直接插入排序比较简单,但是它用途很广泛,其基本实现思路为:
从给定数组的1下标开始,在该下标的左边找比它小的数据,找到后就把小的数据往前移,找完一个下标的,再找另一个下标的。
举个栗子:
给定一个序列array{1,5,3,4,7,2,3},按照直接插入排序的思路把它变为一个升序序列。
如果 j 下标的值大于 i 下标的值,那么就让 j 下标的值往后退一下,即array[j+1] = array[ j ]
当 j 再往前走,发现 j 下标的值小于 i 下标的值,那么 j 停止移动,说明 j 之前已经是排好序的了,此时我们把,存放 i 的值的tmp赋给 j + 1下标,就完成了小的数字前移,大的数字后移:
经过不停地循环,直到……
此时, i 走到数组最后时又找到了一个 3 ,继续刚才的循环,看看这个 3 究竟会被放在哪里:
我们发现,3 被放在了 3 的后面,在排序前,3 也位于 3 的后面,而此时数组的排序已经完成了,那么我们就称这个直接插入排序是一个稳定的排序算法。
稳定性:如果在一个待排序的序列中,有多个相同的元素,在经过排序后,这些元素在序列中的位置相对不变,则称这个排序算法是稳定的;否则为不稳定的。
2、 代码实现:
public static void insertSort(int[] array){
for (int i = 1; i < array.length; i++) {
int j = i - 1;
int tmp = array[i];
for (; j >= 0 ; j--) {
if (array[j] > tmp){
array[j+1] = array[j];
}else {
break;
}
}
array[j+1] = tmp;
}
}
3、 特性总结:
根据直接插入排序的时间复杂度,我们可以得出一个结论:
数据越有序,直接插入排序的速度就越快。
希尔排序应运而生。
二、希尔排序
1、 实现思路:
希尔排序的基本思想:对一个数组进行分组,然后对分组后的每组数据进行直接插入排序,不断地重复这个分组、排序的工作,当最后分为一组排序后,这组数据就成为了有序数组。
Q1:为什么要分组排序呢?
A1:因为分组可以加快排序的速度。
假设我们现在有10000个待排序的数据:
Q2:如何分组?
Q3:两种分组有什么区别呢?
我们对两种分组中的每组数据都进行直接插入排序,得到以下结果:
可以发现,通过希尔排序分组的数据,在排序之后,较大的数字都会放在后边,较小的数字都会放在前边,数组逐渐趋于有序。
而一般情况分组的数据在经过排序后,不一定会达到上述效果。
所以我们选择第二种分组方式。
希尔排序又叫做缩小增量排序,这个增量指的就是每次所分的组的数量,当这个增量不断减小,数组就会不断趋于有序,当增量为1时,再进行一次排序,数组就会变得有序。
那么问题来了~
Q4:如何确定增量?
A4:
希尔排序的分析是一个复杂的问题,因为它的时间是所取“增量”序列的函数,这涉及一些数学上尚未解决的难题。因此,到目前为止尚未有人求得一种最好的增量序列。
——严蔚敏《数据结构(C语言版)》
目前没有一个准确的确定增量的方法,你可以选择gap = n / 2,或者gap = n / 3,或者是其他的,只要让增量逐渐减小到1就行(gap表示增量,n表示数组长度)。
希尔排序本身就是对直接插入排序的一种优化排序,我们只要把刚才的直接插入排序稍微改动一下,即可实现希尔排序:
如果分组后的一组数据内不止2个元素,有多个元素呢?也是同理:
先比较一次红色组的数据,再比较一次绿色组的数据,只要我们每次让 j -= gap ,那么每次比较时,每组数据就不会互相影响:
2、 代码实现:
private static void shell(int[] array, int gap){
for (int i = gap; i < array.length; i++) {
int j = i - gap;
int tmp = array[i]; //把i的值先存起来
for (; j >= 0; j -= gap) {
if (array[j] > tmp) {
array[j + gap] = array[j];//此时需要交换的是j和j+gap下标的值
} else {
break;
}
}
array[j + gap] = tmp;//把tmp的值赋给j+gap下标
}
}
public static void shellSort(int[] array){
int gap = array.length / 2; //自己确定gap的取值
while (gap > 1){
shell(array,gap); //每次分组后都进行一次直接插入排序
gap /= 2;
}
shell(array,gap);//最后一次排序
}
3、 特性总结:
由于增量gap没有一个标准的取值方法,所以希尔排序的时间复杂度是不固定的。
三、选择排序
1、 实现思路:
选择排序又叫直接选择排序,它的实现思路非常简单:每次从待排序的数据中选择最小值(或最大值)放到序列的起始位置,直到全部待排序的数据排完。
2、 代码实现:
public class SelectSort {
public static void swap(int[] array,int i,int j){
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
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);
}
}
}
上述代码中,每次遍历待排序的序列只找到了最小值的下标,如果每次遍历既可以找到最小值的下标,又可以找到最大值的下标,那么排序速度就会变快很多:
public class SelectSort {
public static void swap(int[] array,int i,int j){
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
public static void selectSort2(int[] array){
int left = 0;
int right = array.length - 1;
while (left < right){
int minIndex = left;//记录最小值的下标
int maxIndex = left;//记录最大值的下标
for (int i = left; i < right; i++) {
if(array[i] < array[minIndex]){
minIndex = i;
}
if(array[i] > array[maxIndex]){
maxIndex = i;
}
}
//最小值移动到序列前段
swap(array,left,minIndex);
//判断left处是否为最大值的下标
if(left == maxIndex){
maxIndex = minIndex;
}
//最大值移动到序列末端
swap(array,right,maxIndex);
left++;
right--;
}
}
}
当 maxIndex 和 left 相同时,需要将 maxIndex 指向 minIndex (因为前一步的 swap()方法把 left 和 minIndex 的值进行了交换):
3、 特性总结:
时间复杂度 | O(N^2) |
空间复杂度 | O(1) |
稳定性 | 不稳定 |
四、堆排序
1、 实现思路:
(1)创建一个大根堆(降序创建小根堆);
(2)将堆顶元素(最大值)和待排序元素中的最后一个元素进行交换;
(3)交换完成后向下调整。然后重复第二步,直到排序完所有元素。
2、 代码实现:
public class HeapSort {
public static void swap(int[] array,int i,int j){
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
public void heapSort(int[] array){
createBigHeap(array);
int end = array.length - 1;
while (end > 0){
//交换堆顶元素和最后一个元素
swap(array,0,end);
//向下调整堆顶元素
shiftDown(array,0,end);
end--;
}
}
//创建大根堆
public void createBigHeap(int[] array){
for (int parent = (array.length - 1 - 1) / 2; parent >= 0; parent--) {
shiftDown(array,parent,array.length);
}
}
public 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 += 1;
}
//判断较大的孩子节点的值和双亲节点的值的大小
if(array[child] > array[parent]){
swap(array,child,parent);
//交换完成后需要判断后续的子堆是否为大根堆
parent = child;
child = parent * 2 + 1;
}else {
break;
}
}
}
}
3、 特性总结:
时间复杂度 | O(N*logN) |
空间复杂度 | O(1) |
稳定性 | 不稳定 |
五、冒泡排序
1、 实现思路:
冒泡排序的思路很容易理解:每次从0下标开始,把当前元素和下一个元素进行比较,如果当前元素大于它下一个元素,则进行交换,直到把最大的元素换到待排序序列的最后一个位置。
2、 代码实现:
public class BubbleSort {
public static void swap(int[] array,int i,int j){
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
public void bubbleSort(int[] array){
int len = array.length;
for (int i = 0; i < len - 1; i++) {
//i确定排序的趟数,一共有len个元素,最多排len - 1趟
boolean flg = false;
for (int j = 0; j < len - 1 - i; j++) {
//j确定每趟排序需要比较的元素个数
//每排一趟就减少一个比较的元素
if(array[j] > array[j+1]){
swap(array,j,j+1);
flg = true;
}
}
//如果flg为false,说明这趟排序中一对元素都没有进行交换
//也说明当前序列已经是有序序列了,没必要再进行后续的比较了
if(!flg){
break;
}
}
}
}
3、 特性总结:
时间复杂度 | O(N^2) |
空间复杂度 | O(1) |
稳定性 | 稳定 |
六、快速排序
1、 基本思想:
(1)任取待排序元素中的某一个元素作为基准值;
(2)将当前序列分割成两个子序列,左子序列的元素均小于基准值,右子序列的元素均大于基准值;
(3)左右子序列重复上述两个步骤,直到待排序序列变为有序。
2、 代码实现:
将待排序序列按基准值划分为左右两个部分的常见方式有以下三种:
1、 Hoare版
主要思路:
(1)找基准值下标:从数组末端(right)往前找比数组首元素小的值,再从数组前端(left)往后找比数组首元素大的值,然后交换两个数值,right继续往前找,left继续往后找,直到二者相遇,最后把相遇位置的值和起始位置元素的值进行交换,相遇位置就是基准值所在的下标。
(2)根据基准值,递归基准值左边的子序列,再递归基准值右边的子序列。
public class QuickSort {
public void swap(int[] array,int i, int j){
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
//找基准值下标
public int getPivot(int[] array,int left, int right){
//记录left的起始位置
int temp = 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);
}
//left和right相遇点即基准值下标
//交换起始位置元素和基准值
swap(array,left,temp);
return left;
}
public void hoare(int[] array,int start, int end){
//递归终止条件
if(start >= end){
return;
}
//获取基准值下标
int pivot = getPivot(array,start,end);
//递归基准值左子序列
hoare(array,start,pivot-1);
//递归基准值右子序列
hoare(array,pivot+1,end);
}
public void sort(int[] array){
hoare(array,0, array.length - 1);
}
}
2、 挖坑法
主要思路:
(1)找基准值下标:将当前序列的其实元素存起来,然后从序列末端(right)找比当前序列起始位置小的元素,找到后,把该元素移到序列前端(left)位置,再从序列前端往后找比起始位置大的元素,找到后,把该元素移到right位置。
(2)重复循环上述步骤,直到left和right相遇,相遇点就是基准值下标,把起始位置的元素放到基准值下标处。
(3)根据基准值,递归基准值左边的子序列,再递归基准值右边的子序列。
public class QuickSort2 {
public void swap(int[] array,int i, int j){
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
//找基准值下标
public int getPivot(int[] array,int left, int right){
//存放起始位置元素
int key = array[left];
while (left < right){
//从后往前找比key小的元素
while (left < right && array[right] >= key){
right--;
}
//找到后放到left位置
array[left] = array[right];
//从前往后找比key大的元素
while (left < right && array[left] <= key){
left++;
}
//找到后放到right位置
array[right] = array[left];
}
//left和right相遇位置即基准值下标
//把起始位置元素放到基准值下标处
array[left] = key;
return left;
}
public void hole(int[] array, int start, int end){
//递归终止条件
if(start >= end){
return;
}
//获取基准值下标
int pivot = getPivot(array,start,end);
//递归基准值左子序列
hole(array,start,pivot-1);
//递归基准值右子序列
hole(array,pivot+1,end);
}
public void sort(int[] array){
hole(array,0, array.length - 1);
}
}
3、前后指针法
主要思路:
(1)刚开始时,定义prev指针指向序列起始位置,定义cur指针指向prev的后面一个位置,cur和prev同时向后一步一步走,当cur遇到比起始位置元素大的值时,prev这次不往后走,cur往后走,当cur再次遇到比起始位置元素小的值时,交换prev和cur位置的元素。
(2)当cur遍历完整个序列后,prev所在位置即基准值下标,交换序列起始位置的元素和基准值下标的元素。
(3)根据基准值,递归基准值左边的子序列,再递归基准值右边的子序列。
public class QuickSort3 {
public void swap(int[] array,int i, int j){
int temp = array[i];
array[i] = array[j];
array[j] = temp;
}
//找基准值下标
public int getPivot(int[] array,int left, int right){
//存放起始位置元素
int key = array[left];
int prev = left;
int cur = left + 1;
while (cur <= right){
//cur向后找比key大的值
if(array[cur] < key && array[++prev] != array[cur]){
swap(array,prev,cur);
}
cur++;
}
//遍历完整个数组后,prev所在的位置就是基准值下标
//交换起始位置元素和基准值下标的元素
swap(array,prev,left);
return prev;
}
public void doublePointer(int[] array, int start, int end){
//递归终止条件
if(start >= end){
return;
}
//获取基准值下标
int pivot = getPivot(array,start,end);
//递归基准值左子序列
doublePointer(array,start,pivot-1);
//递归基准值右子序列
doublePointer(array,pivot+1,end);
}
public void sort(int[] array){
doublePointer(array,0, array.length - 1);
}
}
4、快速排序优化
上述三种快速排序的方式,在针对一个有序序列进行排序时,时间复杂度会达到O(N^2),并且,如果序列内的元素过多,递归的深度就会非常深,可能会导致栈溢出,因此,我们需要对上述三种方式进行优化。
(1)三数取中法选基准值
上述三种快速排序的方式,在进行第一次递归时,都是使用了0下标的值来作为初始的基准值,所以在针对有序序列进行排序时,时间复杂度会达到O(N^2),所以,只要我们不使用0下标作为初始基准值即可减少时间复杂度。
三数取中法是从待排序序列的首元素、最后一个元素和中间位置的元素,三个元素中选择一个数值第二大的元素和首元素进行交换,这样就可以在针对有序序列进行排序时,出现时间复杂度为O(N^2)的情况。
//找第二大的数的下标
public int getMidNumIndex(int[] array,int start,int end){
//中间元素的下标
int mid = (start + end) / 2;
int num1 = array[start];
int num2 = array[mid];
int num3 = array[end];
if(num1 > num2){
if(num1 > num3){
if(num2 > num3){
return mid;
}else {
return end;
}
}else {
return start;
}
}else {
//num1 <= num2
if(num2 > num3){
if(num1 > num3){
return start;
}else{
return end;
}
}else {
return mid;
}
}
}
找到第二大的元素后,再把它和首元素进行交换即可:
public void hole(int[] array, int start, int end){
//递归终止条件
if(start >= end){
return;
}
//交换首元素和第二大的元素
int midNumIndex = getMidNumIndex(array,start,end);
swap(array,start,midNumIndex);
//获取基准值下标
int pivot = getPivot(array,start,end);
//递归基准值左子序列
hole(array,start,pivot-1);
//递归基准值右子序列
hole(array,pivot+1,end);
}
(2)递归到元素数量少的子区间时,可以考虑使用插入排序
假设在最好的情况,每次排序都可以通过基准值均分左右子序列,如果将每个子序列看做是一个节点,那么递归的所有结果可以看作一棵完全二叉树:
元素数量越少,递归的次数越多,时间复杂度也就越高,但是此时的子序列已经是趋于有序了,因此我们可以在元素数量较少的时候,使用插入排序来对剩余的元素进行排序,这样就可以大大减少时间复杂度。
public void insertSort(int[] array, int start, int end){
for (int i = start + 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;
}
}
public void hole(int[] array, int start, int end){
//递归终止条件
if(start >= end){
return;
}
if(end - start + 1 < 8){
//子序列的元素小于8个的时候使用直接插入排序
insertSort(array,start,end);
return;
}
//交换首元素和第二大的元素
int midNumIndex = getMidNumIndex(array,start,end);
swap(array,start,midNumIndex);
//获取基准值下标
int pivot = getPivot(array,start,end);
//递归基准值左子序列
hole(array,start,pivot-1);
//递归基准值右子序列
hole(array,pivot+1,end);
}
5、快速排序非递归版
为了彻底解决栈溢出的问题,我们可以使用非递归版的快速排序:
主要思路:
(1)借助一个栈或者队列,把基准值的左、右子序列的起始位置和结束位置分别入队;
(2)每次弹出两个元素作为新区间的起始位置和结束位置,再次找基准值,循环第一步操作;
(3)当栈内为空时结束循环。
public class QuickSort {
//找基准值下标
public int getPivot(int[] array,int left, int right){
//存放起始位置元素
int key = array[left];
while (left < right){
//从后往前找比key小的元素
while (left < right && array[right] >= key){
right--;
}
//找到后放到left位置
array[left] = array[right];
//从前往后找比key大的元素
while (left < right && array[left] <= key){
left++;
}
//找到后放到right位置
array[right] = array[left];
}
//left和right相遇位置即基准值下标
//把起始位置元素放到基准值下标处
array[left] = key;
return left;
}
//非递归版快速排序
public void quickSort(int[] array,int start,int end){
Stack<Integer> stack = new Stack<>();
int pivot = getPivot(array,start,end);
int left = start;
int right = end;
if(left < pivot - 1){
stack.push(left);
stack.push(pivot - 1);
}
if(right > pivot + 1){
stack.push(pivot + 1);
stack.push(right);
}
while (!stack.empty()){
right = stack.pop();
left = stack.pop();
pivot = getPivot(array,left,right);
if(left < pivot - 1){
stack.push(left);
stack.push(pivot - 1);
}
if(right > pivot + 1){
stack.push(pivot + 1);
stack.push(right);
}
}
}
}
3、 特性总结:
时间复杂度(最好) | 时间复杂度(最坏) | 空间复杂度(最好) | 空间复杂度(最坏) | 稳定性 |
O(N*logN) | O(N^2) | O(logN) | O(N) | 不稳定 |
七、归并排序
1、 实现思路:
(1)根据序列长度,将序列均分为左子序列和右子序列;
(2)对第一步进行递归,直到子序列中只有一个元素时进行合并;
(3)合并时,归并排序的问题就会被转换为对两个有序数组进行排序的简单问题。
2、 代码实现:
1、递归版
public class MergeSort {
//分解
public void mergeSort(int[] array,int left,int right){
//终止条件
if(left >= right){
return;
}
int mid = (left + right) / 2;
//1.分解左边
mergeSort(array,left,mid);
//2.分解右边
mergeSort(array,mid + 1,right);
//3.合并
merge(array,left,right,mid);
}
//合并
public void merge(int[] array,int start,int end,int midIndex){
//记录左子序列的起始位置
int ret = start;
//创建一个临时数组
int[] temp = new int[end - start + 1];
//临时数组的下标
int k = 0;
//右子序列的起始位置
int start2 = midIndex + 1;
//两个子序列中都有元素的情况:
while (start <= midIndex && start2 <= end){
if(array[start] <= array[start2]){
temp[k++] = array[start++];
}else {
temp[k++] = array[start2++];
}
}
//只有一个子序列有元素的情况:
while (start <= midIndex){
temp[k++] = array[start++];
}
while (start2 <= end){
temp[k++] = array[start2++];
}
//把temp数组的内容拷贝到array数组中
for (int i = 0; i < temp.length; i++) {
array[ret + i] = temp[i];
}
}
}
2、非递归版
非递归版的归并排序省去了分解的过程,直接开始合并:
public class MergeSort {
public void mergeSort(int[] array){
//gap表示每个子序列的元素个数
int gap = 1;
while (gap <= array.length){
for (int i = 0; i < array.length; i += gap*2) {
//左子序列起始位置
int s1 = i;
//左子序列结束位置
int e1 = i + gap - 1;
//右子序列结束位置
int e2 = e1 + gap;
//如果e2超过数组长度就越界了
if(e2 < array.length){
merge(array,s1,e2,e1);//合并
}
}
gap *= 2;
}
}
//合并
public void merge(int[] array,int start,int end,int midIndex){
//记录左子序列的起始位置
int ret = start;
//创建一个临时数组
int[] temp = new int[end - start + 1];
//临时数组的下标
int k = 0;
//右子序列的起始位置
int start2 = midIndex + 1;
//两个子序列中都有元素的情况:
while (start <= midIndex && start2 <= end){
if(array[start] <= array[start2]){
temp[k++] = array[start++];
}else {
temp[k++] = array[start2++];
}
}
//只有一个子序列有元素的情况:
while (start <= midIndex){
temp[k++] = array[start++];
}
while (start2 <= end){
temp[k++] = array[start2++];
}
//把temp数组的内容拷贝到array数组中
for (int i = 0; i < temp.length; i++) {
array[ret + i] = temp[i];
}
}
}
3、 特性总结:
时间复杂度 | O(N*logN) |
空间复杂度 | O(N) |
稳定性 | 稳定 |