虽说是常用,也只是一些我自己比较经常看到的算法,像折半插入排序、多路归并排序、基数排序桶排序之类的还是点链接看看吧,这里就不写了。
以下主要是多篇博客的总结
十大经典排序算法(动图演示)
常见的7种排序算法
[算法总结] 十大排序算法
1、冒泡排序
冒泡排序主要是把通过比较,把小的元素从下面交换到上面,因此称为“冒泡”(从小到大排)。
算法描述:
- 比较相邻的元素,如果前面的比后面的大,就交换它们两个;
- 对每一对相邻元素作同样的工作,从结尾的最后一对到开始第一对,这样在最前面的元素应该会是最小的数;
- 针对所有的元素重复以上的步骤,除了第一个;
- 重复步骤1~3,直到排序完成。
记得大一上软件工程导论课老师让我们写个冒泡排序,就说我们很多同学写的都是把大的沉下去(包括我自己),这不叫冒泡,这里比较注意这一点。
/*
* array 为待排序数组
* n为数组元素个数
*/
void bubbleSort(int* array, int n)
{
bool needSwap = false;
for (int i = 0; i < n; i++)
{
needSwap = false;
//这趟循环把最小的“泡”“冒”到前面
for (int j = n-1; j > i; j--)
{
if (array[j-1] > array[j])
{
int temp = array[j-1];
array[j-1] = array[j];
array[j] = temp;
needSwap = true;
}
}
//如果比较中没有发生交换,那么就说明已经有序
if (!needSwap)
break;
}
}
时间复杂度平均为O(n^2),最好的情况为O(n)(已经有序,一轮比较中没有发生交换),
最坏的情况为O(n^2)(逆序)。
空间复杂度为O(1)。
算法稳定。
2、简单选择排序
在无序的序列中,先选出最小值(升序排列),放到第一个位置,然后对剩下的元素做同样操作,选出最小值,放到第二个位置,以此类推。
n个记录的直接选择排序可经过n-1趟直接选择排序得到有序结果。具体算法描述如下:
- 初始状态:无序区为R[1…n],有序区为空;
- 第i趟排序(i=1,2,3…n-1)开始时,当前有序区和无序区分别为R[1…i-1]和R(i…n)。该趟排序从当前无序区中-选出关键字最小的记录 R[k],将它与无序区的第1个记录R交换,使R[1…i]和R[i+1…n)分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区;
- n-1趟结束,数组有序化了。
void selectionSort(int *array, int n)
{
for (int i = 0; i < n; i++)
{
int min_index = i;
for (int j = i+1; j < n; j++)
{
if (array[min_index ]> array[j])
{
min_index = j;
}
}
//将第i趟比较中最小的元素与下标i所在元素交换
if (min_index != i)
{
int temp = array[i];
array[i] = array[min_index];
array[min_index] = temp;
}
}
}
时间复杂度平均为O(n^2),
最好的情况和最坏情况都是O(n^ 2)。
空间复杂度为O(1)。
使用例子{2’,2,1,3}测试可以发现算法不稳定。
3、直接插入排序
就像我们打扑克牌时,把每一张牌插到他应该在的位置;插入排序也把序列分成有序和无序两部分,将无序的元素一个个插入到有序序列中应该在的位置。
插入排序还有折半插入和希尔插入,直接插入是对有序部分顺序查找插入,而折半插入则是二分查找插入;希尔排序则是先将序列分组有序,再合并序列,分组有序的方法选择直接插入排序。
相关博客:
理解希尔排序的排序过程
算法描述:
- 从第一个元素开始,该元素可以认为已经被排序;
- 取出下一个元素,在已经排序的元素序列中从后向前扫描;
- 如果该元素(已排序)大于新元素,将该元素移到下一位置;
- 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
- 将新元素插入到该位置后;
- 重复步骤2~5。
void insertSort(int* array, int n)
{
for (int i = 1; i < n; i++)
{
int key = array[i];
int j = i-1;
for (; array[j] > key && j >= 0; j--)
{
array[j+1] = array[j];
}
array[j+1] = key;
}
}
时间复杂度平均为O(n^2),最好的情况O(n)(已经有序),
最坏情况是O(n^ 2)(逆序)。
空间复杂度为O(1)。
算法稳定。
以上算法比较适用于中小规模的数据集,其中插入排序最快,选择排序其次,冒泡最慢。
4、二路归并排序
归并算法一般是递归形式,将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。
算法描述:
- 把长度为n的输入序列分成两个长度为n/2的子序列;
- 对这两个子序列分别采用归并排序;
- 将两个排序好的子序列合并成一个最终的排序序列。
/*
* @start为待排序区间的开始
* @ end为待排序区间的结束位置(最后一个元素所在下标,不同迭代器的end)
* @ array为待排序整个序列
* @ temp为辅助空间
*/
void mergeSort(int start, int end, int* array, int* temp)
{
if (start < end)
{
int mid = (start+end)/2;
mergeSort(start, mid, array, temp);
mergeSort(mid+1, end, array, temp);
//左半部分
int pre_start = start;
int pre_end = mid;
//右半部分
int lat_start = mid+1;
int lat_end = end;
//辅助数组的下标
int index = 0;
//将两个有序序列合并
while (pre_start <= pre_end && lat_start <= lat_end)
{
if (array[pre_start] <= array[lat_start])
{
temp[index++] = array[pre_start++];
}
else
{
temp[index++] = array[lat_start++];
}
}
//余下的单数个元素
while (pre_start <= pre_end)
{
temp[index++] = array[pre_start++];
}
while (lat_start <= lat_end)
{
temp[index++] = array[lat_start++];
}
//合并到原始序列数组中
for (int i = 0; i < index; i++)
{
array[start + i] = temp[i];
}
}
}
每一趟归并的时间复杂度为O(n),共需要进行 logN 趟排序,所以时间复杂度为 O(NlogN),最好的和最坏情况都是O(nlogn)。
空间复杂度为O(n)。
算法稳定。
5、快速排序
快排的思想是先选取序列中一个元素,然后将序列中比这个元素小的放它的左边,比它大的放在右边(升序排列);然后再分别对这两个区间做相同操作,直到区间只有一个元素。
快速排序的关键点在于用作比较的元素的值的选取,一般可以取首位,或末尾,或中间值,较好的取法是三位取中和随机选取。
这里介绍的是取首位的方法。
算法描述:
- 从数列中挑出一个元素,称为 “基准”(pivot);
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
- 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
// 返回选取元素所在的下标,用于左右两边元素后面分区
int partition(int* array, int start, int end)
{
int i = start;
int j = end;
int key = array[start];
while (i < j)
{
while (i < j && array[j] >= key)
j--;
array[i] = array[j];
while (i < j && array[i] <= key)
i++;
array[j] = array[i];
}
array[i] = key;
return i;
}
void qSort(int* array, int start, int end)
{
if (start >= end)
return;
int pivot = partition(array, start, end);
qSort(array, start, pivot - 1);
qSort(array, pivot + 1, end);
}
时间复杂度:最好情况为O(nlogn)(每次都能分成对等的两部分,栈空间为O(logn)),
最坏情况为O(n^2)(当序列已有序,每次只能分为长度为0和n-1的区,此时栈空间为O(n))。平均时间复杂度为 O(NlogN)。
这是对于每次选取首位作为pivot的情况,使用随机选取pivot的方法能使最坏情况得到改善,若排序前先对序列乱序,算法表现更好,但是开销较大。
空间复杂度为O(1)。
有关时间复杂度的讨论可以看看这篇:
快速排序时间复杂度为O(n×log(n))的证明
使用例子{1,3, 2’,2}测试可以发现算法不稳定。
6、堆排序
堆排序对于我来说是最难理解的一种排序算法,它把数组看成一个二叉堆(基于完全二叉树,即给每个元素自上而下自左向右编号,得到的编号与满二叉树一致),基于大顶堆(父节点大于等于左右节点的值)和小顶堆(父节点小于等于左右节点的值)分别可以将序列变成升序序列和降序序列。
基于大顶堆,大顶堆的根节点(下标为0)为堆的最大值,先将序列调整成大顶堆,然后将根节点与末尾元素交换,再将除末尾,剩下的元素重新调整为大顶堆,再把根节点和新末尾元素交换…
过程重点在于如何调整,调整算法基于完全二叉树的性质:
若当前元素标号为i(从0开始算),则其左孩子标号为2 x i+1,右孩子标号为2 x i+2。
相关博客:
图解排序算法(三)之堆排序
漫画:什么是堆排序?
算法描述:
- 将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,此堆为初始的无序区;
- 将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区(Rn),且满足R[1,2…n-1]<=R[n];
- 由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn)。
- 不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。
void heapAdjust(int* array, int parentIndex, int num)
{
int key = array[parentIndex];
for (int k = 2*parentIndex+1; k < num; k = 2*k+1)
{
if (k+1 < num && (array[k+1] > array[k]))
{
k++;
}
if (key >= array[k])
break;
array[parentIndex] = array[k];
parentIndex = k;
}
array[parentIndex] = key;
}
void heapSort(int* array, int num)
{
for (int i = num/2 - 1; i >= 0; i--)
{
heapAdjust(array, i, num);
}
for (int i = num-1; i > 0; i--)
{
int temp = array[i];
array[i] = array[0];
array[0] = temp;
heapAdjust(array, 0, i);
}
}
时间复杂度为O(nlogn),最好和最坏情况都为O(nlogn),
空间复杂度为O(1)。
使用例子{3,2’,2,1}测试可发现算法不稳定。