1、排序的概念及其运用
1.1、排序的概念
排序:所谓排序,就是使一串记录,按照其中的某个或某些关键字的大小,递增或递减的排列起来的操作。
稳定性:假定在待排序的记录序列中,存在多个既有相同的关键字的记录。若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i] = r[j] ,且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前。则称这种排序算法是稳定的;否则称为不稳定。
内部排序:数据元素全部放在内存中的排序。
外部排序:数据元素太多不能同时放在一个内存中,根据排序过程的要求不能在内外存之间移动数据的排序。
1.2、排序的运用
1.3、常见的排序算法
2、插入排序
2.1、基本思想
直接插入排序是一种简单的插入排序法,其基本思想是:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列。
实际中我们完扑克牌时,就是用了插入排序的思想
2.2、直接插入排序
当插入第
i
(i>=1)个元素时,前面的array[0],array[1],......array[i-1]已经排好序,此时用array[i]的排序码与array[i-1],array[i-2],.....的排序码顺序进行比较,找到插入位置即将array[i]插入,原来位置上的元素顺序后移。
//插入排序
void InsertSort(int*a,int n) {
// [0,end] 有序 end+1位置的值插入进去,让[0,end+1]有序
for (int i = 0; i < n-1; ++i)
{
int end= i;
int tmp = a[end + 1];
while (end >= 0)
{
if (a[end] > tmp)
{
a[end + 1] = a[end];
--end;
}
else {
break;
}
a[end + 1] = tmp;
}
}
}
直接插入排序的特性总结:
- 元素集合越接近有序,直接插入排序算法的时间效率越高。
- 时间复杂度:O(N^2^)
- 空间复杂度:O(1),它是一种稳定的排序算法
- 稳定性:稳定
2.3、希尔排序(缩小增量排序)
希尔排序法又称缩小增量法。
基本思想是:先选定一个整数,把待排序文件中的所有记录分成个组,所有距离为
gap
的记录分在同一组内,并对每一组内的记录进行排序。然后,取,重复上述分组和排序的工作。当到达gap
=1时,所有记录在统一组内排好序。
void ShellSort(int* a, int n) {
//int gap = 1;
//////把间隔为gap的多组数据同时排
//for (int i = 0; i < n - gap; ++i)
//{
// int end = i;
// int tmp = a[end + gap];
// while (end >= 0)
// {
// if (a[end] > tmp)
// {
// a[end + gap] = a[end];
// --end;
// }
// else {
// break;
// }
// }
// a[end + gap] = tmp;
//}
//那么问题来了
//gap 到底给多少呢?
int gap = n;
while (gap > 1)
{
//gap = gap / 2; // logN
gap = gap / 3 + 1; // log3N 以3为底数的对数
// gap > 1时都是预排序 接近有序
// gap == 1时就是直接插入排序 有序
// gap很大时,下面预排序时间复杂度O(N)
// gap很小时,数组已经很接近有序了,这时差不多也是(N)
// 把间隔为gap的多组数据同时排
for (int i = 0; i < n - gap; ++i)
{
int end = i;
int tmp = a[end + gap];
while (end >= 0)
{
if (a[end] > tmp)
{
a[end + gap] = a[end];
end-=gap;
}
else {
break;
}
}
a[end + gap] = tmp;
}
}
}
希尔排序的特征总结 :
- 希尔排序是对直接插入排序的优化。
- 当 gap > 1 时都是预排序,目的是为了让数组跟接近与有序。当 gap==1 时,数组已经接近有序,这样就会很快,这样整体而言,可以达到优化的效果。我们实现后可以进行性能测试和对比。
- 希尔排序的时间复杂度不好计算,推导出来平均时间复杂度:O(N^1.3^至N^2^)
- 稳定性:不稳定
3、选择排序
3.1、基本思想
每一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。
3.2、直接选择排序
- 在元素集合array[i]--array[n-1]中选择关键码最大(小)的数据元素
- 若它不是这组元素中的最后一个(第一个)元素,则将它与这组元素中的最后一个(第一个)元素交换
- 在剩余的array[i]--array[n-2](array[i+1]--array[n-1])集合中,重复上述步骤,直到集合剩余1个元素
排序(冒泡排序,选择排序,插入排序,归并排序,快速排序,计数排序,基数排序) - VisuAlgo
直接选择排序的特性总结:
- 直接选择排序思考非常好理解,但是效率不是很好。实际中很少使用
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 稳定性:不稳定
4、堆排序
向下调整算法👇(前提:左右子树都是小堆(大堆))
2.自底向上的建堆方式
该建堆方式是从倒数第二层的节点(叶子节点的上一层)开始,从右向左,从上到下的向下进行调整。 同样的,假设该堆为满二叉树,堆高为。同样的,假设每层高度为,每层节点数为,则 建堆的复杂度为,则有
同样的,该数列和差比数列,因此,可以用错位相减法,得到时间复杂度为
即,空间复杂度为
堆排序(HeapSort)是指利用堆积树(堆)这种数据结构所设计的一种排序算法,它是选择排序的一种。它是通过堆来进行选择数据。需要注意的是排升序要建大堆,排降序建小堆。
堆排序的特征总结:
- 堆排序使用堆来选数,效率就高了很多。
- 时间复杂度:O(N*LogN)
- 空间复杂度:O(1)
- 稳定性:不稳定
5.交换排序 (冒泡、快速)
基本思想:所谓交换,就是 根据序列中两个记录键值的比较结果来对换着两个记录在序列中的位置,交换排序的特点是:将键值较大的记录向序列的尾巴移动,键值较小的记录向序列的前部移动。
5.1 冒牌排序
冒泡排序的特征总结:
- 冒泡排序是一种非常容易理解的排序
- 时间复杂度:O(N^2)
- 空间复杂度:O(1)
- 稳定性:稳定
5.2 快速排序
快速排序是Hoare与1962年提出的一种二叉树结构的交换排序方法,其基本思想未:任取待排序元素序列中的某元素作为基准值,按照该排序码将待排序集合分割成两个序列,左子序列中所有元素均小于基准值,右子序列中所有元素均大于基准数,然后最左右子序列重复该过程,直到所有元素都排列在相对应的位置上为止。
将区间按照基准值划分为左右两半部分的常见方法有:
5.2.1、挖坑法
整体实现思想:
快速排序的特征性总结:
- 快速排序整体的综合性能和使用场景都是比较好的,所以才敢叫快速排序
- 时间复杂度:O(N*LogN)
- 空间复杂度:O(1)
快排什么情况最坏?(有序)时间复杂度是多少?(O(N^2^))
快排的致命缺陷:在有序时效率差不多等于插入排序,那么我们有什么办法去解决呢?
三数取中
int GetMidIndex(int* a, int left, int right){ int mid = (left + right) >> 1; if (a[left] < a[mid]) { if (a[mid] < a[right]) { return mid; } else if (a[left] > a[right]) { return left; } else { return right; } } else { if (a[mid] > a[right]) { return mid; } else if(a[left] < a[right]) { return left; } else { return right; } } return 0; }
这样 咱们快排就基本可以算是一个综合能力非常强的一种排序了。
那么我们再思考,比如最后一层调用为100万次,倒数第二层为50万次,倒数第三层为25万次。我们能不能把这些消除掉呢?
小区间优化
if (prvot - 1 - left >10) { QuickSort(a, left, prvot - 1); } else { InsertSort(a + left, prvot - 1 - left + 1); } if (right - (prvot + 1) > 10) { QuickSort(a, prvot + 1, right); } else { InsertSort(a + prvot + 1, right - (prvot + 1) + 1); }
整体速率提升了
但优化的时间也就几十毫秒,但可以想象一下 几十毫秒 计算机就执行了这么多万次
使用总结:大概在小于500左右开始使用插入排序 效率比较高(10000000)
官方示例为:10-13
总结:小区间优化的区间取值需看整体数量来定,官方所给数量为10-13差不多为最优。
5.2.2、左右指针法
5.2.3、前后指针法
6、归并排序
基本思想:
归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法,该算法是采用了分治法(Divide and Conquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为二路归并。
归并排序核心步骤:
归并排序是稳定排序,它也是一种十分高效的排序,能利用完全二叉树特性的排序一般性能都不会太差。java中Arrays.sort()采用了一种名为TimSort的排序算法,就是归并排序的优化版本。从上文的图中可看出,每次合并操作的平均时间复杂度为O(n),而完全二叉树的深度为|log2n|。总的平均时间复杂度为O(nlogn)。而且,归并排序的最好,最坏,平均时间复杂度均为O(nlogn)。
总结: