文章目录
三大排序—六种算法
前言
在日常生活中,排序是我们会经常遇到的问题。而学习数据结构与算法,排序也是绕不过去的坎。这里介绍了插入排序,选择排序,交换排序,以及基于这三种排序的改良版。和我一起来学习这三种排序方法吧。
一、插入排序
1、算法简介
先默认下标为0的数值最小,从下标为1的地方开始,若发现当前数值比前面的数值tmp小,则插入到这个数值tmp之前。形象一点来说,就是像小时候排队。先随便找一个孩子,让他成为队首,然后第二个小朋友和队首比较身高,如果比队首高,就站在队首后面,若比队首矮,则站在队首的前面,成为新队首。然后第三个孩子继续从队首开始比较,找到比他高的,就站在比他高的孩子的前面,没有就站到队尾。依次类推,直到所有的孩子排完队。
这就像在找自己位置一样,不是就往后继续找,找到了就坐在那!先给出代码。
void insertSort(int *data, int count) {
int i;
int j;
int t;
int tmp;
for (i = 1; i < count; i++) {
tmp = data[i];//先把找座位的保护起来
for (j = 0; j < i && tmp >= data[j]; j++) {
}//找到座位
for (t = i; t > j; t--) {
data[t] = data[t - 1];
}//让座位后面的人向后移动,像不像插队呢-_-
data[j] = tmp;//让这个人做到座位上
}
}
2、算法分析
二、希尔排序(改良后的插入排序)
1、算法简介
希尔排序是基于插入排序的改良版之一。由于插入排序对有序数据的排序速度快,希尔排序就是先将数据变得基本有序。
基本思想是将n个数据逐次分为两半。就是先设置一个步长n/2。然后每个数据和这个数据+步长的数据进行插入排序。循环结束,步长/2…直到步长为1。
就是像跳跃一般,每次和相等步长的数进行比较,这样不但会将比较小的放在前面,还会减少了许多的移动空间的操作。
void ShellSort(int *data, int count) {
int start;
int step;
//外循环控制步长长度,内循环从0到步长的位置
for (step = count/2; step; step /= 2) {
for (start = 0; start < step; start++) {
shellOneSort(data, count, start, step);
}
}
}
//希尔排序的一步,原型就是插入排序
void shellOneSort(int *data, int count, int start, int step) {
int i;
int j;
int t;
int tmp;
for (i = start + step; i < count; i += step) {
tmp = data[i];
for (j = start; j < i && tmp >= data[j]; j += step) {
}
for (t = i; t > j; t -= step) {
data[t] = data[t - step];
}
data[j] = tmp;
}
}
2、算法分析
最差情况也是O(n*n),即完全倒序,比较和赋值与插入排序相同。
由于计算希尔排序的时间复杂度,涉及现在还未解决的数学难题。所以这里只给出他的时间复杂度。
希尔排序的时间复杂度是:O(nlogn)~O(n2),平均时间复杂度大致是O(1.5n)。
三、选择排序
1、算法简介
从第一个数据开始,找所有数据中的最小值,若与第一个数据不同,则这两个数据进行交换。然后再从第二个数据开始……
选择排序的原理是比较简单的。找最小数、交换等等。
代码实现:
void selectSort(int *data, int count) {
int i;
int j;
int minIndex;
int tmp;
for (i = 0; i < count - 1; i++) {
minIndex = i;
//寻找最小数下标
for (j = i; j < count; j++) {
if (data[j] < data[minIndex]) {
minIndex = j;
}
}
//最小下标不等于minIndex时,进行交换。
if (minIndex != i) {
tmp = data[i];
data[i] = data[minIndex];
data[minIndex] = tmp;
}
}
}
2、算法分析
从代码分析,无论如何,都会进行n*(n - 1)次比较,所以没有高潮也没有低潮,那么这里的交换就成了相对于比较而言比较重要的点。
最优情况:完全顺序,只比较,不交换
最差情况:完全逆序,比较且交换。
综上所述:
时间复杂度为O(n*n)
空间复杂度为O(1)
四、堆排序(改良后的选择排序)
1、算法简介
堆排序是基于大根堆(升序)和小根堆(降序)
堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆。
下面图片来自https://www.cnblogs.com/chengxiao/p/6129630.html
实际上,我们可以将数组看作一个完全二叉树,实际上也确实如此。
每次生成大根堆时,从完全二叉树的最后一个叶子节点开始,对一个父节点进行生成大根堆的处理。然后从最后一父节点开始,依次对每个父节点做相似处理。最后会使得最大元素恰好就是根节点,再将根节点于最后一个叶子节点进行交换。下次的生成大根堆时,不把排序好的数据计入大根堆。
看完图再简单总结下堆排序的基本思路:
a.将无需序列构建成一个堆,根据升序降序需求选择大顶堆或小顶堆;
b.将堆顶元素与末尾元素交换,将最大元素"沉"到数组末端;
c.重新调整结构,使其满足堆定义,然后继续交换堆顶元素与当前末尾元素,反复执行调整+交换步骤,直到整个序列有序。
代码实现:
void adjustHeap(int *data, int count, int root) {//处理一个根下面的节点及根节点。使之成为一个大根堆
int left;
int right;
int maxNode;
int tmp;
while (root <= count / 2 - 1) {
left = 2*root + 1;
right = left + 1;
maxNode = right >= count ? left
: (data[left] > data[right]
? left : right);//确定根节点是否存在右孩子,以及左右孩子的最大点
maxNode = data[root] > data[maxNode]
? root : maxNode;//比较根节点与左右孩子中的最大点的大小
if (maxNode == root) {//若最大点已经是根节点,直接返回即可
return;
}
tmp = data[root];
data[root] = data[maxNode];
data[maxNode] = tmp;
root = maxNode;//以最大点为根节点,继续比较
}
}
void heapSort(int *data, int count) {
int root;
int tmp;
//先使二叉树变为大根堆
for (root = count / 2 - 1; root > 0; root--) {
adjustHeap(data, count, root);
}
//每次取出大根堆中的根节点,然后使他“沉”入数组“末端”
while (count) {
adjustHeap(data, count, 0);//每次以根节点为起点,生成大根堆
tmp = data[0];
data[0] = data[count - 1];
data[count - 1] = tmp;
--count;
}
}
2、算法分析
堆排序的时间复杂度分为初始化堆和排序重建堆。
初始化堆阶段:
初始化堆是从下向上,从左向右进行的。假设高度为k,则从倒数第二层右边的节点开始,开始执行比较交换;倒数第二层,则会选择其子节点进行比较,若进行了交换,则继续选择子节点的子节点开始比较。上层也是这样。
总的时间:s = 2 ^ (i - 1) * (k - i); i表示第几层,2 ^ (i - 1)表示该层上有多少个元素, (k - i)表示子树要下调的深度。
则s = 2 ^ (k -2) * 1 + 2 ^ (k -3) * 2 +…… + 2 ^ (k -i) *(i -1) + 2 ^(0) *(k - 1)(叶子层不需交换),则i从k-1到1。
s = 2 ^ k -k -1,由因为k为完全二叉树的深度,而log(n) = k。
得到: S = n - log(n) -1,所以时间复杂度为O(n)
排序重建堆
每次重建意味着有一个节点出堆,所以需要将堆的容量减一。adjustheap()函数的时间复杂度k=log(n),k为堆的层数。所以在每次重建时,随着堆的容量的减小,层数会下降,函数时间复杂度会变化。重建堆一共需要n-1次循环,每次循环的比较次数为log(i),则相加为:log2+log3+…+log(n-1)+log(n)≈log(n!)。可以证明log(n!)和nlog(n)是同阶函数:
∵(n/2)^(n/2) ≤ n! ≤ n^n
∴n/4log(n) = n/2log(n^(1/2)) ≤ n/2log(n/2) ≤ log(n!)≤nlog(n)
所以时间复杂度为O(nlogn)
则总的时间复杂度为O(n+nlogn)=O(nlogn)。
在此借鉴堆排序的时间复杂度分析
五、交换排序
1、算法简介
交换排序又名冒泡排序,就是让相邻两个数据进行比较,若前面的数据大于后面的数据,则进行交换。就像大泡泡往水底沉一样。
可以很清楚的看到,每次从头开始,截至到n-i(n为长度,为第几次循环)
代码如下:
void swapSort(int *data, int count) {
int i;
int j;
int tmp;
int hasSwap = 1;//hasSwap判断某次循环是否有交换,若没有交换,则说明已经排好序,这是直接结束循环
for (i = 0; hasSwap && i < count - 1; i++) {
hasSwap = 0;
for (j = 0; j < count - i - 1; j++) {
if (data[j] > data[j+1]) {
tmp = data[j];
data[j] = data[j+1];
data[j+1] = tmp;
hasSwap = 1;
}
}
}
}
2、算法分析
明显的可以看出:时间复杂度为O(n*(n -1) / 2);
最差情况:完全倒序
最优情况:完全顺序。
六、快速排序
1、算法简介
快速排序实际上是以头为基准,运用头指针和尾指针比较基准,使比基准小的数摆放在基准的左边,比基准大的数排放在基准的后面,然后此时这组数据已经被分为两半,然后除基准外的数,作为一个新的数据,继续执行前面的过程。
做法:
1.用tmp记录这组数据的第一个元素。
2.使head和tail分别指向数据的第一个元素和最后一个元素。
3.由于tmp已经记录了这个数,则以head为下标的数据可以视作空。
4.用tail指向的空间数据与tmp进行比较,若tail所指向的空间的数据大于等于tmp,则tail–,继续第4步;
5.若tail所指向的空间的数据小于tmp,则交换head所指向空间的数据和tail所指向的空间数据,同时head++;
6.用head指向的空间数据与tmp进行比较,若tail所指向的空间的数据小于等于tmp,则head++,继续第6步;
7.若head所指向的空间的数据大于tmp,则交换head所指向空间的数据和tail所指向的空间数据,同时tail–,执行第4步;
4~7一直只想到head > tail时,结束。
这样将每次的数据分为两半,然后再进行比较,我们可以使用递归来解决这个问题。
void quickOnce(int *data, int count, int head, int tail) {
int tmp = data[head];//设置基准
int start = head;
int end = tail;
if (head >= tail) {
return;
}
while (head < tail) {
while (head < tail && data[tail] >= tmp) {
--tail;
}
if (head < tail) {
data[head] = data[tail];
++head;
}
while (head < tail && data[head] <= tmp) {
++head;
}
if (head < tail) {
data[tail] = data[head];
--tail;
}
}
data[head] = tmp;
//分别递归基准左边的右边的数
quickOnce(data, count, start, head - 1);
quickOnce(data, count, head + 1, end);
}
void qucikSort(int *data, int count) {
quickOnce(data, count, 0, count - 1);
}
2、算法分析
时间复杂度为:O(nlogn)。
由于笔者水平有限,现直接给出别人的解答。
七、总结
这几天对这几类算法进行了复习,当自己手动去画出这些代码的步骤时,理解起来更加深刻。俗话说的好:眼过千遍,不如手过一遍。