本周将会介绍几种常见的排序算法。
简单排序
简单排序是几个简单的排序算法的统称,下面来一一介绍。
冒泡排序
冒泡排序的思想比较直观,每次循环时,会将数组(或链表)内相邻的两个元素进行比较,按照规定的递增(或递减)顺序向后移动,一直到重复到末尾,基本代码如下:
1 | void Bubble_Sort(ElementType A[], int N) { |
代码框架十分简单,但是这样会有一个问题,那就是待排序列在已经有序的情况下,依然会进行两次循环,尽管不会交换相邻元素的值,但是还是会进行判断,会白白浪费时间。仔细想一下,如果输入序列是有序的,那么元素的值一次也不会交换,根据这个特点,可以对上述的代码进行一点小优化,添加一个标志位,一次有序后就可以直接跳出循环了。具体代码如下:
1 | void Bubble_Sort(ElementType A[], int N) { |
按照上述代码的思路,可以较为清晰的分析出冒泡排序算法的时间复杂度:
- 最好的情况,序列为顺序序列,
- 最坏的情况,序列为逆序序列,
另外,对于冒泡排序而言,每一次冒泡结束(内层循环结束一轮)后都会有一个元素被放到这个序列有序后的最终位置上,而且,冒泡排序也不会交换相同元素的位置(使用而不是),所以冒泡排序是稳定的排序算法,再者,冒泡排序还有一个优点,即排序的方向是一定的,只会按照一个方向遍历存储数据的数据结构,这是其他排序算法无法达到的。
插入排序
插入排序有个很形象的例子,就是打扑克牌时“理牌”的过程,不过可能有点差别。区别在于,理扑克牌时,手上是没有牌的,而需要理的牌在牌堆里面,也就是说,有两个空间可以放牌,但插入排序实际上只使用了一个内存空间,这样的话,每一次插入时就需要先找到插入的位置了。
按照“理牌”的过程来描述就是:先摸一张牌,从后往前找到合适的插入位置,将比这张牌大的牌向后移(腾出位置),再将新牌插入到这个位置下即可。基本代码如下:
1 | void Insertion_Sort(ElementType A[], int N) { |
插入排序和冒泡排序的时间复杂度类似:
- 最好的情况,序列为顺序序列,
- 最坏的情况,序列为逆序序列,
同时,插入排序也是稳定的排序算法。
时间复杂度下界
对于下标,如果,则称是一对逆序对(Inversion)如序列中,和分别是一对逆序对,逆序对的个数称为逆序数,这与线性代数中的概念是一致的。
在冒泡排序和插入排序中,每次交换位置的两个相邻元素正好消去 1 个逆序对,那么它们的时间复杂度就是:,其中是逆序对的对数。很明显,如果序列基本有序,则值可忽略不计,时间复杂度仅为,此时算法既简单,又高效。
定理:对于任意N个不同元素组成的序列平均具有个逆序对。
由上面的定理,我们可以得出:任何仅以交换相邻两元素来排序的算法,其平均时间复杂度为。这意味着,如果想要提高算法的效率,就得在每次交换元素时,不止消去 1 个逆序对,这就要求每次交换的两个元素要在序列中相隔的比较远。
希尔排序
希尔排序的主要目的就是每次交换元素时,通过消去多个逆序对来达到提升算法效率。其主要思想:先定义增量序列,然后对每个进行“-间隔”排序(),值得注意的是,后面进行的“间隔”排序不会影响前面“间隔”排序的有序性,也即“-间隔”有序的序列,在执行“-间隔”排序后,仍然是“-间隔”有序的。
原始希尔排序,增量依次减半,,此时最坏情况下的时间复杂度为,基本代码如下:
1 | void Shell_Sort(ElementType A[], int N) { |
在最坏的情况(每次进行间隔排序的序列都是有序的)下,希尔排序会退化成插入排序,原因在于增量元素不互质,则小增量可能根本不起作用。为了解决这个问题,引入了更多的增量序列:
- Hibbard增量序列,,这样保证了相邻增量元素互质,最坏情况下,猜想
- Sedgewick增量序列,,也即增量序列的每一个元素是由或计算得到,猜想
由于希尔排序每次对增量序列进行排序,相邻的相等元素属于不同增量序列,则希尔排序可能会改变相等且相邻元素的相对位置,属于不稳定的排序。
选择排序
选择排序是一种简单直观的排序算法,基本工作原理是每一次从待排序的数据元素中选出最小(大)的一个元素,存放在序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾,重复此过程直到全部待排序的数据元素排完。
简单选择排序
简单选择排序的思路很简单,与选择排序的基本思想一致,基本代码如下:
1 | void Selection_Sort(ElementType A[], int N) { |
简单选择排序的时间复杂度主要有两个影响条件,第一个是查找最小值,第二个是交换,所以它的时间复杂度无论怎样都是。
堆排序
分析了简单选择排序算法的时间复杂度后,发现简单选择排序的时间都耗费在了查找最小值上,如果能把这一操作变快,那么简单选择排序的效率就能提高。根据选择排序的特点,每次查找的值必须是最小(大)值,这与小(大)根堆的性质是一致的,那么使用堆来进行元素的查找,每次弹出最小(大)值,就可以提升排序效率。基本代码如下:
1 | void Heap_Sort(ElementType A[], int N) { |
上述代码的过程比较简单,每次找出最小(大)元素后,保存在一个临时数组内,然后在将临时数组内的排序结果复制到原始数组中。这样即需要额外的空间()去存储这部分数据,又要额外的时间去复制元素。那么如何去避免这部分开销呢?请看下面的代码:
1 | void Heap_Sort(ElementType A[], int N) { |
按照上述代码,建立一个大根堆,排序开始后,交换大根堆最大元素与最末尾元素的值,完成后,将这个最大值剔除出堆,紧接着再调整为大根堆,重复执行上述操作。
定理:堆排序处理N个不同元素的随机排列的平均比较次数是
虽然堆排序给出最佳平均时间复杂度,但实际效果不如用Sedgewick增量序列的希尔排序。
归并排序
归并排序的核心就是有序子列的归并,首先,申请空间,保存合并后的有序序列,设定两个指针,最初位置分别为两个有序序列的起始位置,比较两个指针指向的元素,选择相对小的元素放到申请的空间内,并移动指针到下一位置,重复直至某一个子序列超出序列尾,接着将另一序列剩下的所有元素直接复制到申请空间的尾部。合并两个有序子列的基本代码如下:
1 | void Merge(ElementType A[], ElementType TempA[], int L, int R, int RightEnd) { |
归并算法在实现的时候有两种不同的策略,下面先介绍递归。
递归
归并排序的递归算法的思想是基于分治法的,先将分开的子序列排好,在合并成一个大的序列。基本代码如下:
1 | void MSort(ElementType A[], ElementType TempA[], int L, int RightEnd) { |
基于分治法的归并排序递归算法的时间复杂度为,推导过程如下:
可得,这个时间复杂度是很“强”的😆,也即是说在任何情况下(无论好坏)都是,另外,归并排序是稳定的排序算法。为了使它与上述其他排序算法的函数接口统一,再整理下代码:
1 | void Merge_Sort_Recursion(ElementType A[], int N) { |
注意在上面的代码中,临时传递使用的数组是声明在Merge_Sort
函数中的,这样做的好处就是避免了在MSort
中重复声明和重复释放内存操作。
非递归
归并排序的非递归算法基本思想依然是分治法的思想,每次循环先归并相邻的两个子列,子列长度逐渐增加,直至最后左右两个子列之和大于序列总长度。基本代码如下:
1 | void Merge_Sort_Non_Recursion(ElementType A[], int N) { |
注意上述代码中的细节,Merge_pass
是一个按照序列长度进行一次归并的函数,当序列长度发生变化的时候,将再次调用此函数,另外,在此函数内,for
循环内归并的序列是前N/length - 1
对,而不是N/length
对,如果N
是奇数,最后一个子列就被单独出来了,它与其他子列的长度不等,所以针对最后一个子列的处理,要与前面区分开来。再者,上述代码中length
增长的倍数是2
,理论上即是二路归并。
小结
归并排序算法的优点很明显,那就是在任何情况(无论好坏)下,其时间复杂度都是,同时,它还是稳定的排序算法,但是它有一个很明显的缺点,就是需要占用大小的空间,并且在内存内要频繁的进行倒换操作,所以,一般内部排序中不会使用归并排序,外部排序会使用归并排序。
Homework
09-1 排序
这个题专门用来检测自己实现的排序算法,最好把老师讲了的都实现一下。
冒泡排序
1 |
|
插入排序
1 |
|
希尔排序
1 |
|
堆排序
1 |
|
归并排序
非递归
1 |
|
递归
1 |
|
09-2 Insert or Merge
这个题形式简单,但是要想得满分,必须得对插入排序跟归并排序都很了解才行。
题目要求输出两样东西:
- 判断属于那一种排序
- 然后输出再进行下一次这种排序后所得的序列
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
int tar[maxn], arr[maxn], tmparr[maxn], n;
bool issame(int *a) {
for(int i = 0; i < n; i++) {
if(a[i] != tar[i]) return false;
}
return true;
}
void printarray(const int *array, int size) {
for(int i = 0; i < n; i++) {
printf("%d", array[i]);
if(i < n - 1) putchar(' ');
}
}
void insert_pass(int *array, int pos) {
int tmp = array[pos], j;
for(j = pos; j > 0 && tmp < array[j - 1]; j--) {
array[j] = array[j - 1];
}
array[j] = tmp;
}
bool isinsert(int *array, int n, int *round) {
for(int i = 1; i < n; i++) {
insert_pass(array, i);
if(issame(array)) {
*round = i + 1;
return true;
}
}
return false;
}
void merge(int *array, int *tmparr, int left, int right, int rightend) {
int leftend = right - 1, tmp = left;
int numofelements = rightend - left + 1;
while(left <= leftend && right <= rightend) {
if(array[left] < array[right]) tmparr[tmp++] = array[left++];
else tmparr[tmp++] = array[right++];
}
while(left <= leftend) tmparr[tmp++] = array[left++];
while(right <= rightend) tmparr[tmp++] = array[right++];
for(int i = 0; i < numofelements; i++, rightend--) {
array[rightend] = tmparr[rightend];
}
}
void merge_pass(int *array, int *tmparr, int n, int length) {
int i, j;
for(i = 0; i <= n - 2 * length; i += (2 * length)) {
merge(array, tmparr, i, i + length, i + 2 * length - 1);
}
if(i + length < n) merge(array, tmparr, i, i + length, n - 1);
else for(j = i; j < n; j++) tmparr[j] = array[j];
}
void merge_sort(int *array, int n) {
int *tmparr;
tmparr = (int*)malloc(n * sizeof(int));
if(tmparr != NULL) {
int length = 1;
while(length < n) {
merge_pass(array, tmparr, n, length);
length *= 2;
if(issame(tmparr)) {
merge_pass(tmparr, array, n, length);
printarray(array, n);
break;
}
merge_pass(tmparr, array, n, length);
length *= 2;
if(issame(array)) {
merge_pass(array, tmparr, n, length);
printarray(tmparr, n);
break;
}
}
free(tmparr);
} else return;
}
int main() {
scanf("%d", &n);
for(int i = 0; i < n; i++) {
scanf("%d", &arr[i]);
tmparr[i] = arr[i];
}
for(int i = 0; i < n; i++) {
scanf("%d", &tar[i]);
}
int round = 0;
if(isinsert(tmparr, n, &round)) {
printf("Insertion Sort\n");
insert_pass(tmparr, round);
printarray(tmparr, n);
} else {
for(int i = 0; i < n; i++) tmparr[i] = arr[i];
printf("Merge Sort\n");
merge_sort(tmparr, n);
}
return 0;
}
/*
samples:
in:
10
3 1 2 8 7 5 9 4 6 0
1 2 3 7 8 5 9 4 6 0
out:
Insertion Sort
1 2 3 5 7 8 9 4 6 0
in:
10
3 1 2 8 7 5 9 4 0 6
1 3 2 8 5 7 4 9 0 6
out:
Merge Sort
1 2 3 8 4 5 7 9 0 6
*/
09-3 Insertion or Heap Sort
这个题与上题类型一致,只不过把归并排序换成了堆排序。
1 |
|