排序
推荐可视化工具
排序的基本概念
相关概念
排序(sort) 就是重新排列表中的元素,使表中的元素满足按关键字有序的过程。
输入∶ n个记录R1, R2…Rn,对应的关键字为k1, k2… ., kn。
输出︰ 输入序列的一个重排R1’, R2’…Rn’,使得有k1’≤k2’≤…≤kn’(也可递减)
排序算法的评价指标
- 时间复杂度
- 空间复杂度
- 稳定性(稳定的排序算法不一定比不稳定的好,根据需求而定)
排序算法的分类
- 内部排序——数据都在内存中(关注如何使算法时、空复杂度更低)
- 外部排序——数据太多,无法全部放入内存(还要关注如何使读/写磁盘次数更少)
插入排序
直接插入排序和带哨兵插入排序
算法思想: 每次将一个待排序的记录按其关键字大小插入到前面已排好序的子序列中,直到全部记录插入完成。
不带哨兵的数组存储形式
带哨兵的数组存储形式
void InsertSort(int a[], int n) { //不带哨兵 ,n为数组大小
int i, j, temp;
for (i = 1; i < n; i++) {
if (a[i] < a[i - 1]) { //i指向待排序元素
temp = a[i]; //temp为待排序元素
for (j = i - 1; j >= 0 && a[j] > temp; j--) { //和前面的进行比较 ,如果前面的比此时的大
a[j + 1] = a[j]; //大于temp的元素后移,后面的元素a[j+1]依次被前面的元素a[j]覆盖
}
a[j + 1] = temp; //最后空位赋值temp
}
}
}
//哨兵位于a[0],数据存储在a[0]~a[n]
void InsertSort(int a[], int n) { //带哨兵,n为数组存储的元素的个数
int i, j;
for (i = 2; i <= n; i++) { //依次将a[2]~a[n]插入到前面已排序序列
if (a[i] < a[i - 1]) { //若a[i]关键码小于其前驱,将A[i]插入有序表
a[0] = a[i]; //复制为哨兵,A[0]不存放元素
for (j = i - 1; a[0] < a[j]; j--) { //从后往前查找行
a[j + 1] = a[j]; //向后挪位
}
a[j + 1] = a[0]; //复制到插入位置
}
}
}
算法效率分析
最坏时间复杂度:——O(n2)
最好时间复杂度——O(n)
算法稳定性︰稳定
折半插入排序
折半插入排序的数组存储形式
具体操作:
①在将一个新元素插入已排好序的数组的过程中,寻找插入点时,将待插入区域的首元素设置为a[low],末元素设置为a[high]。
②将待插入元素与a [mid] (其中mid=(low+high)/2)相比较,如果比参考元素小,则选择a[low]到a[mid-1]为新的插入区域(即high=mid-1),否则选择a[mid+1]到a[high]为新的插入区域(即low=mid+1)
③如此直至low<=high不成立,即将high+1位置及之后所有元素后移一位,并将新元素插入a[high+1]。
void InsertionSort(int a[], int n) { //n为数组存储的元素的个数
int i, j, low, high, mid;
for (i = 2; i <= n; i++) //依次将a[2]~a[n]插入到前面已经排好序的列表
{
a[0] = a[i]; //将a[i]暂存到a[0]
low = 1;
high = i - 1;
while (low <= high) {
mid = (low + high) / 2; //取中间位置(利用low + (high - low) / 2求mid是为了防止整数溢出问题)
if (a[mid] > a[0]) //查找左子表
{
high = mid - 1;
} else //查找右子表
{
low = mid + 1;
}
}
for (j = i - 1; j >= high + 1; --j) //i - 1指待插入元素的前一个元素,即有序列表中所有大于待插入元素的最后一个元素;high + 1指有序列表中所有大于待插入元素的第一个元素
{
a[j + 1] = a[j]; //统一后移元素
}
a[high + 1] = a[0]; //插入操作
}
}
希尔排序(Shell Sort)
希尔排序的数组存储形式
希尔排序︰先将待排序表分割成若干形如 L[i,i + d, i + 2. …, i + kd]的“特殊”子表,对各个子表分别进行直接插入排序。缩小增量d,重复上述过程,直到d=1为止。
-
希尔排序︰先追求表中元素部分有序,再逐渐逼近全局有序
-
步骤 定义数组a[9],a[0]不存储:①确定增量d(d的取值不唯一,一般是4,2,1)
②d=4时,可将数组分为(a[1],a[5]) (a[2],a[6]) (a[3],a[7]) (a[4],a[8])四组,每组直接插入排序后重新分组
③d=2时,经过d=4的分组排序后,又分成(a[1],a[3],a[5],a[7])(a[2],a[4],a[6],a[8])两组,每组进行直接插入排序后重新分组
④d=1时,直接a[9]数组直接插入排序
-
代码
void ShellSort(int a[], int n) { //n为数组存储的元素的个数
int d, i, j;
//A[0]只是暂存单元,不是哨兵,当j<=0时,插入位置已到
for (d = n / 2; d >= 1; d /= 2) {
for (i = d + 1; i <= n; i++) {
if (a[i] < a[i - d]) { //需将A[i]插入有序增量子表
a[0] = a[i]; //暂存在A[0]
for (j = i - d; j > 0 && a[0] < a[j]; j -= d) { //记录后移,查找插入的位置
a[j + d] = a[j]; //插入
}
a[j + d] = a[0];
}
}
}
}
空间复杂度: O(1)
时间复杂度 ∶和增量序列d1, d2, d3…的选择有关,目前无法用数学手段证明确切的时间复杂度最坏时间复杂度为O(n2),当n在某个范围内时,可达O(n1.3)
稳定性: 不稳定!
适用性: 仅适用于顺序表,不适用于链表
交换排序
交换排序的数组存储形式
冒泡排序
从后往前(或从前往后)两两比较相邻元素的值,若为逆序〈即A[i-1]>A[i]),则交换它们,直到序列比较完。称这样过程为“—趟”冒泡排序。总共需进行n-1趟冒泡。
void BubbleSort(int a[], int n) { //n为数组大小
for (int i = 0; i < n - 1; i++) {
for (int j = 1; j < n - i; j++) {
if (a[j] < a[j - 1]) {
int temp = a[j - 1];
a[j - 1] = a[j];
a[j] = temp;
}
}
}
}
空间复杂度:O(1)
最好时间复杂度=O(n)
最坏时间复杂度=O(n2)
平均时间复杂度=O(n2)
稳定性:稳定
适用性:顺序表、链表都可以
快速排序
-
基本思想:
通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据都小于或者等于另外一部分的所有数据,然后再按照此方法对这两部分数据分别进行快速排序,整个排序的过程可以递归进行,以此达到整个数据变成有序序列。 -
步骤:
①设置两个变量low=0,high=n-1;
②设置中枢轴pivot=a[0](推荐),也可以随机数组中的元素
③从high开始向前搜索,即由后开始向前搜索(high- -),找到第一个小于pivot的值a[high],将A[high]和A[low]的值交换;
④从low开始向后搜索,即由前开始向后搜索(low++),找到第一个大于pivot的值a[low],将a[low]和a[high]的值交换;
⑤重复第③、④步,直到low==high; -
图解
-
代码
//快速排序
int Partition(int a[], int high, int low) {
int pivot = a[low]; //第一个元素作为中枢轴
while (low < high) { //用low、high搜索枢轴的最终位置
while (low < high && a[high] >= pivot) {
high--;
}
a[low] = a[high]; //比枢轴小的元素移动到左端
while (low < high && a[low] <= pivot) {
low++;
}
a[high] = a[low]; //比枢轴大的元素移动到右端
}
a[low] = pivot; //枢轴元素存放到最终位置
return low; //返回存放枢轴的最终位置
}
void QuickSort(int a[], int low, int high) {
if (low < high) { //递归跳出的条件
int pivotpos = Partition(a, low, high); //划分,其值代表一趟划分后的位置
QuickSort(a, low, pivotpos - 1); //划分左子表
QuickSort(a, pivotpos + 1, high); //划分右子表
}
}
简单选择排序
简单选择排序的数组存储形式
-
思想:每一趟在待排序元素中选取关键字最小的元素加入有序子序列
-
图解
-
代码
void SelectSort(int a[], int n) {
int i, j;
for (i = 0; i < n; i++) {
int min = i; //记录最小元素位置
for (j = i + 1; j < n; j++) { //在a[i]后寻找比a[i]小的元素
if (a[j] < a[min]) {
min = j; //记录下标值
}
}
if (min != i) { //交换下标min和i的值
int temp = a[i];
a[i] = a[min];
a[min] = temp;
}
}
}
堆排序
什么是堆
若n个关键字序列L[ 1…n]满足下面某一条性质,则称为堆((Heap) :
①若满足∶L(i)≥L(2i)且L(i)≥L(2i+1) (1 ≤ i ≤n/2)——大根堆(大顶堆)根结点数值最大
②若满足∶L(i)≤L(2i)且L(i)≤L(2i+1) (1 ≤i ≤n/2 )——小根堆(小顶堆)根结点数值最小
如何建立大小根堆(大根堆为例)
-
起初根据数组建立对应的二叉树
-
把所有非终端结点(有分支的结点) 都检查一遍,是否满足大根堆的要求,如果不满足,则进行调整(a[1 2 ……n/2])。n为结点个数
①检查当前结点是否满足根≥左、右,若不满足,将当前结点与更大的一个孩子互换
注:i的左孩子——2i,i的右孩子—2i+1,i的父节点——i/2
②注意下坠,重新调整位置 -
代码:
//建立大根堆
void BuildMaxHeap(int a[], int len) {
for (int i = len / 2; i > 0; i--) {
HeadAdjust(a, i, len);
}
}
//将以k为根的子树调整为大根堆
void HeadAdjust(int a[], int k, int len) { //len为结点的个数
a[0] = a[k]; //a[0]暂存根结点的值
for (int i = 2 * k; i < len; i *= 2) { //沿key较大的子结点向下筛选
if (i < len && a[i] < a[i + 1]) { //判断k为根结点的左右孩子谁更大
i++; // 右孩子大则i+1
}
if (a[0] > a[i]) { //判断此时k的值和最大的孩子结点谁更大
break; //满足大根堆,break
} else {
a[k] = a[i]; //不满足,a[i]调整到双亲结点
k = i; //修改k的值,一遍继续筛选
}
}
a[k] = a[0]; //被筛选的结点放入最终位置
}
如何排序
①每一趟将堆顶元素加入有序子序列(与待排序序列中的最后一个元素交换)
②大根堆中删除堆顶元素,重新建立大根堆
③反复①③操作
void HeapSort(int a[], int len) {
BuildMaxHeap(a, len); //第一次建堆
for (int i = len; i > 1; i--) { //n-1趟交换和建堆
Swap(a[i], a[1]); //堆顶元素和堆底元素交换
HeadAdjust(a, 1, i - 1); //把剩余的待排序元素整理成堆
}
}
在堆中插入\删除新元素(代码可以写不重要)
对于大根堆,新元素放到表尾,与父节点对比,若新元素比父节点更大,则将二者互换。新元素就这样一路“上升”,直到无法继续上升为止
被删除的元素用堆底元素替代,然后让该元素不断“下坠”,直到无法下坠为止
注意:对比次数的计算
归并排序
代码:
void Merge(int a[], int low, int mid, int high) { //high为最后一个元素的下标
int b[high + 1], i, j, k; //b数组用于复制a数组
for (i = low; i <= high; i++) { //a复制到b中
b[i] = a[i];
}
for (i = low, j = mid + 1, k = i; i <= mid && j <= high; k++) {
if (b[i] <= b[j]) { //将小的放入a中
a[k] = b[i++];
} else {
a[k] = b[j++];
}
}
while (i <= mid) {
a[k++] = b[i++];
}
while (j <= high) {
a[k++] = b[j++];
}
}
void MergeSort(int a[], int low, int high) {
if (low < high) {
int mid = (low + high) / 2;
MergeSort(a, low, mid);
MergeSort(a, mid + 1, high);
Merge(a, low, mid, high);
}
}
基数排序(要求仅手动模拟)
①
②第一趟看个位,个位是数字几,就把该数值放到对应的队列中
③第二趟看十位,十位是数字几,就把该数值放到对应的队列中
④第三趟看百位,百位是数字几,就把该数值放到对应的队列中
假设长度为n的线性表中每个结点aj的关键字由d元组(kjd-1, kjd-2,kjd-3,. . .,kj1 , kj0)组成
其中,0<=kji<=r-1(0<=j<n,0<=i<=d-1) ,r称为“基数”
基数排序得到递减序列的过程如下,
1.初始化:设置r个空队列,Qr-1,Qr-2,…,Qo
2. 按照各个关键字位权重递增的次序(个、十、百),对d个关键字位分别做“分配”和“收集”
3. 分配:顺序扫描各个元素, 若当前处理的关键字位=x,则将元素插入Qx队尾
4. 收集:把Qr-1,Qr-2,…,Qo各个队列中的结点依次出队并链接
基数排序得到递增序列的过程如下,
1.初始化:设置r个空队列,Q0,Q1,……,Qr-1
5. 按照各个关键字位权重递增的次序(个、十、百),对d个关键字位分别做“分配”和“收集”
6. 分配:顺序扫描各个元素, 若当前处理的关键字位=x,则将元素插入Qx队尾
7. 收集:把Q0,Q1,……,Qr-1各个队列中的结点依次出队并链接
外部排序
外部排序:数据元素太多,无法一次全部读入内存进行排序
使用归并排序的方法,最少只需在内存中分配3块大小的缓冲区即可对任意一个大文件进行排序,外存中分成小块进入内存块中排序后在放入外存中。
败者树
假设天津饭由派大星替代,并不会重新比拼,只会进行三次比拼,减少了对比次数
经过一次对比后的图:
置换选择排序
最佳归并树
相当于哈夫曼树的构造,小到大排序,依次排序,然后构造
例如构造1 2 2 5 6的最佳归并树