目录
0 摘要
排序是计算机程序设计大赛中最常见,最基础却又是最为重要的知识点。在实际生活中也有着极其重要的作用。主要介绍排序的种类:希尔排序、冒泡排序、快速排序、堆排序等等,以及各种算法的比较及其适应的情况。
1 引言
假设含有n个记录的序列为{r1,r2,...rn},其相应的关键字分别为{k1,k2,...,kn},需确定1,2,...,n的一种排列p1,p2,...,pn,使其相应的关键字满足kp1≤kp2≤...≤kpn非递减(或非递增)关系,及时地序列称为一个按关键字有序的序列{rp1,rp2,...,rpn},这样的操作就称为排序。
2 排序算法
2.1 排序算法的分类
2.1.1 稳定排序与不稳定排序
稳定排序:对任意一组数据元素序列,使用某种排序算法对它进行按照关键字的排序,若相同关键字的前后位置关系在排序前与排序后保持一致,则称此排序方法时稳定的。
不稳定排序:对任意一组数据元素序列,使用某种排序算法对它进行按照关键字的排序,若相同关键字的前后位置关系在排序前与排序后不能保持一致,则称此排序方法时不稳定的。
例如:关键字序列{3(1),4,2,3(2),1},经过排序算法后变为{1,2,3(1),3(2),4},则次排序方法是稳定的;若变为{1,2,3(2),3(1),4},则次排序方法时不稳定的。
2.1.2 内排序与外排序
按照排序过程中所涉及的存储器的不同可分为内排序和外排序。
内排序是指待排序序列完全存放在内存中进行的排序过程,这种方法适合数量不太大的数据元素的排序。
外排序是指待排序的数据元素非常多,以至于它们必须存储在外部存储器上,这种排序过程中需要在内外存之间多次交换数据才能进行,这样的排序称为外排序。
2.2 排序算法的性能评价
时间复杂度:排序算法的时间开销是衡量其好坏的最重要的标志。高效率的内排序算法应该是具有尽可能少的关键字比较次数和尽可能少的记录移动次数。
空间复杂度:辅助存储空间是除了存放待排序所占用的存储空间之外,执行算法所需要的其他存储空间。当排序算法中使用的额外内存空间与要排序数据元素的个数n无关时,其空间 复杂度为O(1),大多数排序算法的空间复杂度是O(1),也有一些时间复杂度性能好的排序算法,其空间复杂度会达到O(n)。
稳定性,即对任意一组数据元素序列,使用某种排序算法对它进行按照关键字的排序,若相同关键字的前后位置关系在排序前与排序后保持一致,则称此排序方法时稳定的。稳定的排序算法是应用问题所希望的,因此,排序算法的稳定性是衡量排序算法好坏的一个重要指标。
2.3 内排序和外排序的举例
内排序:插入排序、交换排序、选择排序和归并排序。
外排序:磁盘排序中的多路平衡归并排序、置换-选择排序和最优归并树。
2.4 内排序的四种分类
3 算法比较
3.1 冒泡排序
冒泡排序是一种交换排序,他的基本思想是:两两比较相邻记录的关键字,如果反序则交换,知道没有反序的记录为止。对于标准的冒泡排序的代码,我觉得应该有改进的地方,还是可以优化的,(如果序列有序,但是标准算法依然还是进行大量比较,就显得很多余)。那么我就给出优化后的代码:
void BubbleSort2(SqList *L)
{
Int i, j;
Status flag = TRUE;
for (i = 1; i < L->length && flag; i++)
{
Flag = FALSE;
for (j = L->length - 1; j >= i; j--)
If(L->r[j] > L -> r[j+1]
{
Swap(L, j, j + 1);
Flag = TRUE;
}
}
}
代码改动的关键是在i变量的for循环中,增加了对flag是否为TRUE的判断。
3.2 简单选择排序
简单选择排序法(Simple Selection Sort)就是通过n-i次关键字间的比较,从 n-i+1个记录中选出关键字最小的记录,并和第i (1<i<n)个记录交换之。
void SelectSort(SqList *L)
{
int i, jrmin;
for (i <= l; i < L->length; i++)
{
min = i; for (j = i + 1; j <= L->length; j++) /*循环之后的数据 */
{
if (L->r[min] > L->r[j])
min = j;
}
if (i != min)
swap(L, i, min);
}
}
3.3 直接插入排序
直接插入排序(Straight Insertion Sort)的基本操作是将一个记录插入到已经排 好序的有序表中,从而得到一个新的、记录数增1的有序表。
void InsertSort(SqList *L)
{
int i, j;
for (i = 2; i < -L->length; i++)
{
if (L->r[i] < L->r(i - 1))
L->r[O] = L->r[i];
}
for (j = i - 1; L->r[j] > L->r[0]; j--)
{
L->r[j + 1] = L->r(j);
L->r[j + 1] - L->r[0];
}
}
3.4 希尔排序
现在,我要讲解的算法叫希尔排序(Shell Sort)o希尔排序是D.LShell于1959年 提出来的一种排序算法,在这之前排序算法的时间复杂度基本都是年n的平方的,希尔排序算法是突破这个时间复杂度的第一批算法之一。
void ShellSort(SqList *L)
{
int i , j;
int increment = L->length;
do
{
increment = increment / 3 + l;
for (i = increment + l; i <= L->length; i++)
if (L->r[i] < L->r[i - increment])
{
L->r[0] - L->r[i];
for (j - (i - increment); j > 0 && L->r[0] < L->r[j]; j <= increment)
L->r[j + increment] = L->r[j]; /*记录后移,查找插入位置 */
L->r[j + increment] = L->r[0]; /* 插入 */
}
} while (increment > l);
}
3.5 堆排序
堆排序(Heap Sort)就是利用堆(假设利用大顶堆)进行排序的方法。它的基本思想是,将待排序的序列构造成一个大顶堆。此时,整个序列的最大值就是堆顶的根 结点。将它移走(其实就是将其与堆数组的末尾元素交换,此时末尾元素就是最大值),然后将剩余的n-1个序列重新构造成一个堆,这样就会得到n个元素中的次小值。如此反复执行,便能得到一个有序序列了。
void HeapSort(SqList *L)
{
int i;
for (i = L->length / 2; i > 0; i--)
HeapAdj ust(L, i, L->length);
for (i = L->length; i > l; i--)
{
swap(L.l, i);
HeapAdjust(L, lri - 1);
}
}
3.6 归并排序
归并” 一词的中文含义就是合并、并入的意思,而在数据结构中的定义是将两个 或两个以上的有序表组合成一个新的有序表。
归并排序(Merging Sort)就是利用归并的思想实现的排序方法。它的原理是假设初始序列含有n个记录,则可以看成是n个有序的子序列,每个子序列的长度为1,然后两两归并,得到[n/2] ([x]表示不小于x的最小整数)个长度为2或1的有 序子序列;再两两归并,……,如此重复,直至得到一个长度为n的有序序列为止, 这种排序方法称为2路归并排序。
void MSort(int SR[], int TRI[], int s, int t)
{
int m;
int TR2[MAXSIZE + 1];
if (s == t)
TRl[s] = SR[s];
else
{
m = (s + t) / 2;
MSort(SR, TR2, s, m);
MSort(SR, TR2, m + 1, t);
Merge(TR2, TR1, s, m, t); /*将TR2[s.・m]和TR2[m+1..仁]*/
}
}
3.7 快速排序
快速排序(Quick Sort)的基本思想是:通过一趟排序将待排记录分割成独立的 两部分,其中一部分记录的关键字均比另一部分记录的关键字小,则可分别对这两部 分记录继续进行排序,以达到整个序列有序的目的。
void QSort(SqList *Lr, int low, int high)
{
int pivot;
if (low < high)
{
pivot = Partition(L, low, high);
QSort(L, low, pivot - 1);
QSort(L, pivot + 1, high);
}
}
4 总结回顾
本章文只是在讲排序,我们需要对已经提到的各个排序算法进行对比来总结回 顾。
首先我们讲了排序的定义,并提到了排序的稳定性,排序稳定对于某些特殊需求 来说是至关重要的,因此在排序算法中,我们需要关注此算法的稳定性如何。
我们根据将排序记录是否全部被放置在内存中,将排序分为丙排序与外排序两 种,外排序需要在内外存之间多次交换数据才能进行。我们本章主要讲的是内排序的 算法。
根据排序过程中借助的主要操作,我们将内排序分为:插入排序、交换排序、选 择排序和归并排序四类。之后介绍的7种排序法,就分别是各种分类的代表算法。
我们对各种排序方法进行总结,对比。
通过这个表,我们直接的能进行下列的分析:
从算法的简单性来看,我们将7种算法分为两类:
- 简单算法:冒泡、简单选择、直接插入。
- 改进算法:希尔、堆、归并、快速。
从平均情况来看,显然最后3种改进算法要胜过希尔排序,并远远胜过前3种简 单算法。
从最好情况看,反而冒泡和直接插入排序要更胜一筹,也就是说,如果你的待排 序序列总是基本有序,反而不应该考虑4种复杂的改进算法。
从最坏情况看,堆排序与归并排序又强过快速排序以及其他简单排序。
从这三组时间复杂度的数据对比中,我们可以得出这样一个认识。堆排序和归并 排序就像两个参加奥数考试的优等生,心理素质强,发挥稳定。而快速排序像是很情 绪化的天才,心情好时表现极佳,碰到较糟糕环境会变得差强人意。但是他们如果都 来比赛计算个位数的加减法,它们反而算不过成绩极普通的冒泡和直接插入。
从空间复杂度来说,归并排序强调要马跑得快,就得给马吃个饱。快速排序也有 相应的空间要求,反而堆排序等却都是少量索取,大量付出,对空间要求是0(1)。如 果执行算法的软件所处的环境非常在乎内存使用量的多少时,选择归并排序和快速排 序就不是一个较好的决策了。
从稳定性来看,归并排序独占鳌头,我们前面也说过,对于非常在乎排序稳定性 的应用中,归并排序是个好算法。
从待排序记录的个数上来说,待排序的个数n越小,采用简单排序方法越合适。 反之,n越大,采用改进排序方法越合适。这也就是我们为什么对快速排序优化时, 增加了一个阀值,低于阀值时换作直接插入排序的原因。
5 例题介绍
我们可以使用基于快速排序的方法,求出「出现次数数组」的前k大的值。
在对数组arr[做快速排序的过程中,我们首先将数组划分为两个部分arri...q-1]与arrq+1.j,并使得arriq-1中的每一个值都不超过arr[q],且arrq+1..j]中的每一个值都大于arrg]。于是,我们根据k与左侧子数组arri...q-1]的长度(为q-i)的大小关系:
·如果k≤q-i,则数组arrl...r]前k大的值,就等于了数组arri...q-1前k大的值。
。否则,数组arrlr]前k大的值,就等于左侧子数组全部元素,加上右侧了数组arrq+1...jj中前 k-(q-i)大的值。
原版的快速排序算法的平均时间复杂度为O(NlogN)。我们的算法中,每次只需在其中的一个分支递归即可,因此算法的平均时间复杂度降为O(N)。
代码如下:
#include <bits/stdc++.h>
using namespace std;
struct hash_table
{
int key;
int val;
UT_hash_handle hh;
};
typedef struct hash_table *hash_ptr;
struct pair
{
int first;
int second;
};
void swap(struct pair *a, struct pair *b)
{
struct pair t = *a;
*a = *b, *b = t;
}
void sort(struct pair *v, int start, int end, int *ret, int *retSize, int k)
{
int picked = rand() % (end - start + 1) + start;
swap(&v[picked], &v[start]);
int pivot = v[start].second;
int index = start;
for (int i = start + 1; i <= end; i++)
{
if (v[i].second >= pivot)
{
swap(&v[index + 1], &v[i]);
index++;
}
}
swap(&v[start], &v[index]);
if (k <= index - start)
{
sort(v, start, index - 1, ret, retSize, k);
}
else
{
for (int i = start; i <= index; i++)
{
ret[(*retSize)++] = v[i].first;
}
if (k > index - start + 1)
{
sort(v, index + 1, end, ret, retSize, k - (index - start + 1));
}
}
}
int *topKFrequent(int *nums, int numsSize, int k, int *returnSize)
{
hash_ptr head = NULL;
hash_ptr p = NULL, tmp = NULL;
for (int i = 0; i < numsSize; i++)
{
HASH_FIND_INT(head, &nums[i], p);
if (p == NULL)
{
p = malloc(sizeof(struct hash_table));
p->key = nums[i];
p->val = 1;
HASH_ADD_INT(head, key, p);
}
else
{
p->val++;
}
}
struct pair values[numsSize];
int valuesSize = 0;
HASH_ITER(hh, head, p, tmp)
{
values[valuesSize].first = p->key;
values[valuesSize++].second = p->val;
}
int *ret = malloc(sizeof(int) * k);
*returnSize = 0;
sort(values, 0, valuesSize - 1, ret, returnSize, k);
return ret;
}
6 文章结尾
本篇主要详述了排序算法的概要以及排序算法在相关竞赛的应用。陆续会更新其他竞赛中常用的一些知识点。
7 文件分享
如果大家需要Word或者PDF版本,使用以下链接。
链接:https://pan.baidu.com/s/1ZE0VwwFqTdbk7WyMAUZ3aA
提取码:lh91