-
- 外排序:需要在内外存之间多次交换数据才能进行
- 内排序:
- 插入类排序
- 直接插入排序
- 希尔排序
- 选择类排序
- 简单选择排序
- 堆排序
- 交换类排序
- 冒泡排序
- 快速排序
- 归并类排序
- 归并排序
- 插入类排序
排序方法 | 平均情况 | 最好情况 | 最坏情况 | 辅助空间 | 稳定性 |
---|---|---|---|---|---|
冒泡排序 | O(n^2) | O(n) | O(n^2) | O(1) | 稳定 |
简单选择排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | 稳定 |
直接插入排序 | O(n^2) | O(n) | O(n^2) | O(1) | 稳定 |
希尔排序 | O(nlogn)~O(n^2) | O(n^1.3) | O(n^2) | O(1) | 不稳定 |
堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) | 不稳定 |
归并排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) | 稳定 |
快速排序 | O(nlogn) | O(nlogn) | O(n^2) | O(logn)~O(n) | 不稳定 |
/* 直接插入排序函数的实现
* array[] : 待排序的数组
* length : 待排序的数组的大小
*/
void insertion_sort(int array[], int length)
{
int i, j;
int temp; // 用来存放临时的变量
for(i = 1; i < length; i++)
{
temp = array[i];
for(j = i-1; (j >= 0)&&(array[j] > temp); j--)
{
array[j + 1] = array[j];
}
array[j + 1] = temp;
}
}
编写测试代码如下所示:
/* 程序的入口函数 */
int main()
{
int a[ARRAY_LENGTH];
int i;
int d[3] = {5, 3, 1}; // 定义一个表示增量值的数组
/* 输入10个整形元素 */
printf("Input %d numbers : \n", ARRAY_LENGTH);
for(i = 0; i < ARRAY_LENGTH; i++)
{
scanf("%d", &a[i]);
}
printf("****************************************************************\n");
/* 把排序前元素都打印出来 */
printf("The elements before sort is : \n");
for(i = 0; i< ARRAY_LENGTH; i++)
{
printf("%d ", a[i]);
}
printf("\n");
printf("****************************************************************\n");
/* 对元素进行有小到大的直接插入排序 */
insertion_sort(a, ARRAY_LENGTH);
/* 把排序后元素都打印出来 */
printf("The elements after sort is : \n");
for(i = 0; i < ARRAY_LENGTH; i++)
{
printf("%d ", a[i]);
}
printf("\n");
return 0;
}
2)希尔排序
在1959年D.L.Shell正式提出了我们今天的主角shell算法,这是相当酷的一件事情,为什么这么说呢?因为shell排序时第一个突破了O(n^2)时间复杂度的排序算法,这应该是排序算法历史上比较闪耀的时刻了,因为在1959年之前的相当长的一段时间里,各种各样的排序算法无论是怎么花样繁多,都始终无法突破O(n^2)雷池一步,在当时直接插入排序已经是相当优秀的了,而排序算法不可能突破O(n^2)的声音成为了当时的主流.
看见了这段历史之后你有什么感受呢,我们在课堂上不愿意学的算法确仍然是科学家们多年苦苦思索才发明出来的,是不是觉得他们很不容易呢?其实你也没必要内疚啦,即使你曾经对它不屑一顾过,没有认真的学它,So what?现在开始学也不晚是不是?
- 插入排序在对几乎已经排好序的数据操作时,效率高,即可以达到线性排序的效率。
- 但插入排序一般来说是低效的,因为插入排序每次只能将数据移动一位。
shell算法的核心还是分组,但是这个分组就有门道儿了,因为它会实现取一个小于总数据长度的整数值gap作为分组的步长,什么意思呢?假如我们的待排序数组为:
序号 1 2 3 4 5 6 7 8 9 10
49,38,65,97,76,13,27,49,55,04
设置gap的值为长度10的一半也就是5,那么第一个和第六个元素就是一组,第二个和第七个元素就是一组,第三个和第八个元素就是一组,第四个和第九个元素就是一组,第五个和第十个元素就是一组,所以一共分为了gap = 5组,
组 一 二 三 四 五
序号 1 6 2 7 3 8 4 9 5 10
数据 49 13 38 27 65 49 97 55 76 04
交换后 13 49 27 38 49 65 55 97 04 76
然后如上面每一组之间进行再直接插入排序,比较如果前一个元素比较大,则交换两个元素的位置,直至5组全部交换完毕.此时数组的顺序为
13 27 49 55 04 49 38 65 97 26.
然后gap的值再减半为2,重新分组,也就是第一个 第三个 第五个 第七个 第九个 元素为第一组是13 49 4 38 97, 第二个 第四个 第六个 第八个 第十个元素为一组是27 55 49 65 26.
组 一 二
序号 1 3 5 7 9 2 4 6 8 10
数据 13 49 04 38 97 27 55 49 65 26
交换后 04 13 38 49 97 26 27 49 55 65
然后如上面对它们两个组分别进行直接插入排序,得到结果为
4 26 13 27 38 49 49 55 97 65,
之后gap的值再减半为1(要知道gap的值小于1的时候在分组就没意义了,一位你的每一个组至少要有一个元素才能组成一个序列.)这次我们直接对上一次的结果进行一次真正的直接插入排序(为什么说是真正的呢,因为此时步长已经为1)直至得出最终结果:
4 13 26 27 38 49 49 55 65 97.
下面是shell算法的实现代码:
-
void shell_sort(int array[], int length){
-
int i;
-
int j;
-
int k;
-
int gap; //gap是分组的步长
-
int temp; //希尔排序是在直接插入排序的基础上实现的,所以仍然需要哨兵
-
for(gap=length/ 2; gap> 0; gap=gap/ 2){
-
for(i= 0; i<gap; i++){
-
for(j=i+gap; j<length; j=j+gap){ //单独一次的插入排序
-
if( array[j] < array[j - gap]){
-
temp = array[j]; //哨兵
-
k = j - gap;
-
while(k>= 0 && array[k]>temp){
-
array[k + gap] = array[k];
-
k = k - gap;
-
}
-
array[k + gap] = temp;
-
}
-
}
-
}
-
}
-
}
这是完全按照希尔算法的思想写的,并没有做任何更改.但还是有几点要说一下
- 希尔排序的时间复杂度为O(n*logn).
- 我们怎么确定步长才能使算法达到最高效呢?其实这是一个很严谨的数学证明问题,可惜的是我们的科学家们目前为止并没有寻找到一个唯一的答案,但是根据维基百科的介绍,还是有几种比较高效的步长的,大家如果感兴趣的话可以到这个链接看一下:希尔排序-维基百科.
- 希尔排序是不稳定的,可以通过我们上面的两个相同的49就可以看得出来.(其中一个49已经被下划线标记了下来)
二、选择类排序
1)简单选择排序
简单选择排序的算法实现思想是:第一趟,从n个记录当真找出 关键字最小的记录与第一个记录交换;第二趟,从第二个记录开始的n-1个记录中找出关键字最小的记录与第二个记录交换;依次类推,直到整个序列按照关键字有序。
下面实现一个简单的选择排序函数(按照逐渐递增方式进行排序):
-
/* 选择排序算法的实现
-
* array[] : 待排序的数组
-
* length : 待排序的数组的长度
-
*/
-
void selection_sort(int array[], int length)
-
{
-
int i, j, m;
-
int temp; // 用于存放临时待排序的元素值
-
-
for(i = 0; i < length -1; i++)
-
{
-
m = i;
-
for(j = i + 1; j < length; j++)
-
{
-
if( array[j] < array[m])
-
m = j;
-
}
-
if(m != i)
-
{
-
temp = array[i];
-
array[i] = array[m];
-
array[m] = temp;
-
}
-
}
-
}
-
/* 程序的入口函数 */
-
int main()
-
{
-
int a[ARRAY_LENGTH];
-
int i;
-
-
/* 输入10个整形元素 */
-
printf( "Input %d numbers : \n", ARRAY_LENGTH);
-
for(i = 0; i < ARRAY_LENGTH; i++)
-
{
-
scanf( "%d", &a[i]);
-
}
-
-
printf( "****************************************************************\n");
-
-
/* 把排序前元素都打印出来 */
-
printf( "The elements before sort is : \n");
-
for(i = 0; i< ARRAY_LENGTH; i++)
-
{
-
printf( "%d ", a[i]);
-
}
-
printf( "\n");
-
-
printf( "****************************************************************\n");
-
-
/* 对元素进行有小到大的顺序进行排序 */
-
selection_sort(a, ARRAY_LENGTH);
-
-
/* 把排序后元素都打印出来 */
-
printf( "The elements after sort is : \n");
-
for(i = 0; i < ARRAY_LENGTH; i++)
-
{
-
printf( "%d ", a[i]);
-
}
-
printf( "\n");
-
-
return 0;
-
}
编译运行结果如下:
2)堆排序
1 // 最大堆排序:获取左孩子函数
2 int left(const int i)
3 {
4 return (2*i+1);
5 }
6
7 // 最大堆排序:获取右孩子函数
8 int right(const int i)
9 {
10 return (2*i+2);
11 }
12
13 // 最大堆排序:在某一节点重新生成最大堆函数
14 void max_heapify(int *input, const int i, const int heap_len)
15 {
16 int l = left(i);
17 int r = right(i);
18 int largest = i;
19 int tem = 0;
20
21 if( l < heap_len && input[l]>input[largest] )
22 {
23 largest = l;
24 }
25
26 if( r < heap_len && input[r]>input[largest] )
27 {
28 largest = r;
29 }
30
31 if( largest != i )
32 {
33 tem = input[largest];
34 input[largest] = input[i];
35 input[i] = tem;
36 max_heapify(input,largest,heap_len);
37 }
38
39 return;
40 }
41
42 // 最大堆排序:构建最大堆函数
43 void build_max_heap(int *input, const int len)
44 {
45 int i = len/2-1;
46
47 for(; i >= 0 ; --i)
48 {
49 max_heapify(input, i , len);
50 }
51
52 return;
53 }
54
55 // 最大堆排序:主函数
56 void maxheap_sort(int *input, const int len)
57 {
58 int tem = 0;
59 int heap_len = len;
60
61 // 将输入数组生成最大堆
62 build_max_heap(input,len);
63
64 while( heap_len > 0)
65 {
66 // 交换根节点与最末尾节点
67 tem = input[0];
68 input[0] = input[heap_len-1];
69 input[heap_len-1] = tem;
70
71 // 在根节点重新生成最大堆
72 --heap_len;
73 max_heapify(input, 0, heap_len);
74 }
75
76 return;
77 }
三、交换类排序
1)冒泡排序
int bubble_sort(int *input, const int len)
2 {
3 int i,j;
4 int tem;
5
6 if(NULL == input || len < 0)
7 {
8 return -1;
9 }
10
11 for(i=0 ; i<len ; ++i)
12 {
13 for(j = len-1 ; j>i ; --j)
14 {
15 if(input[j]<input[j-1])
16 {
17 tem = input[j];
18 input[j] = input[j-1];
19 input[j-1] = tem;
20 }
21 }
22 }
23 return 0;
24 }
2)快速排序
// 快速排序:主函数
2 void sort(int *input, const int p, const int r)
3 {
4 if(p<r)
5 {
6 // 将输入数组分割成两部分
7 int q = partition(input, p, r);
8
9 // 递归调用,分别排序两部分
10 sort(input, p, q-1);
11 sort(input, q+1, r);
12 }
13
14 return;
15 }
16
17 // 快速排序:分割函数
18 int partition(int *input, const int p, const int r)
19 {
20 int key = input[r];
21 int i = p-1;
22 int j = p;
23 int tem = 0;
24
25 for(; j < r ; ++j)
26 {
27 if( input[j] < key )
28 {
29 ++i;
30 tem = input[i];
31 input[i] = input[j];
32 input[j] = tem;
33 }
34 }
35
36 tem = input[i+1];
37 input[i+1] = key;
38 input[r] = tem;
39
40 return i+1;
41 }
三、归并类排序
1)归并排序
概述:额,还是举个栗子吧:
初始序列[ 98 , 1 , 23 , 4 , 2 , 9 , 8 , 18]
//第一步[ 98 | 1 | 23 | 4 | 2 | 9 | 8 | 18]
//第二步[ 1 98 | 4 23 | 2 9 | 8 18]
//第三步[ 1 4 23 98 | 2 8 9 18]
//第三步[ 1 2 4 9 18 23 98]
排序完成:
时间复杂度:o(nlog(2)n)
核心代码:
void Merge(int A[],int Temp[],int L,int R,int RightEnd)//合并两个有序序列
{
int LeftEnd=R-1;
int p=L,i;
int num=RightEnd-L+1;
while(L<=LeftEnd&&R<=RightEnd)
if(A[L]<=A[R])
Temp[p++]=A[L++];
else
Temp[p++]=A[R++];
while(L<=LeftEnd)
Temp[p++]=A[L++];
while(R<=RightEnd)
Temp[p++]=A[R++];
for(i=0;i<num;i++,RightEnd--)
A[RightEnd]=Temp[RightEnd];
}
void MSort(int A[],int Temp[],int L,int RightEnd)
{
int center;
if(L<RightEnd)
{
center=(L+RightEnd)/2;
MSort(A,Temp,L,center);
MSort(A,Temp,center+1,RightEnd);
Merge(A,Temp,L,center+1,RightEnd);
}
}
void Merge_sort(int A[],int N)
{
int *Temp=(int *)malloc(N*sizeof(int));
if(Temp)
{
MSort(A,Temp,0,N-1);
free(Temp);
}
else
printf("no space!\n");
}
测试样例
int main(){
int A[]={1,3,63,5,78,9,12,52,8};
printf("Previous Arrary:");
for(int i=0;i<9;++i)
printf(" %d",A[i]);
Merge_sort(A,9);
printf("\nSorted Arrary:");
for(int i=0;i<9;++i)
printf(" %d",A[i]);
printf("\n");
return 0;
}
运行结果:
Previous Arrary: 1 3 63 5 78 9 12 52 8
Sorted Arrary: 1 3 5 8 9 12 52 63 78
请按任意键继续. . .
四、计数排序
1 // 计数排序:主函数
2 void count_sort(int *input, int *output, const int len)
3 {
4 int max = find_max(input, len);
5 int i;
6
7 int *tem = (int *)malloc(sizeof(int) * (max + 1));
8 for(i = 0; i < max+1 ; ++i)
9 tem[i] = 0;
10
11 // 记录各个元素出现次数
12 for(i = 0 ; i < len ; ++i)
13 tem[input[i]]++;
14
15 // 对记录数据进行修改:从最前边,两两相加,计算小于等于某个元素的元素个数
16 for(i = 1 ; i < max+1 ; ++i)
17 tem[i] += tem[i-1];
18
19 // 将元素直接放入输出数组
20 for(i = 0 ; i < len ; ++i)
21 {
22 output[ tem[input[i]] - 1 ] = input[i];
23 tem[input[i]]--;
24 }
25 return;
26 }
27
28 // 计数排序:寻找最大值函数
29 int find_max(int *input, const int len)
30 {
31 int max = 0;
32 int i;
33
34 for(i = 0 ; i < len ; ++i)
35 if( input[i]>max )
36 max = input[i];
37
38 return max;
39 }
常见算法的总结:
序号 | 名称 | 简述 | 复杂度 | 特性 | 注意事项 | 改进 |
1 | 冒泡排序 | 从数组最后开始,两两比较元素(n-1) 如果顺序不对,则交换元素位置 一轮过后,冒一个泡到当前首元素位置 减小数组长度,进入下一轮(n) | 最好:O(n2) 平均:O(n2) 最差:O(n2) 空间:O(1) | 稳定 就地排序 比较排序 | 冒泡方向与比较方向相配合 冒泡停止条件 | 如果前一轮没有交换过数据,则立刻停止 局部冒泡排序 鸡尾酒排序(双向冒泡) 奇偶排序(a[j],a[j+1]一轮j为奇数,一轮为偶数,在多处理器条件下,实现很快) 梳排序(定义gap,每次减1) |
2 | 选择排序 | 每轮找到最小值(n-1) 与本轮首元素交换 减小数组长度进入下一轮(n) | 最好:O(n2) 平均:O(n2) 最差:O(n2) 空间:O(1) | 不一定稳定 就地排序 比较排序 | 每轮的首元素位置 | 堆排序 |
3 | 插入排序 | 从后边拿出一个元素 与前边各个元素比较,如果不符合要求,则依次向后移动前边的元素(n) 符合要求后,放下这个元素 加长数组长度,进入下一轮(n) | 最好:O(n) 平均:O(n2) 最差:O(n2) 空间:O(1) | 稳定 就地排序 比较排序 | 插入元素的位置 插入元素下标越界 | 二分查找排序,通过二分查找,更快找到插入位置 希尔排序,每次比较前进更多 |
4 | 希尔排序 | 规定增量,下标为增量倍数的元素为一组 各组进行插入排序 选择更小的增量,进入下一轮,直到为1 | 最好:O(n) 平均:与增量有关 最差:O(nlogn2) 空间:O(1)
| 不稳定 就地排序 比较排序 | 下标加入增量后的越界 插入元素位置 | 增量序列选择 1、4、10、23…… |
5 | 合并排序 | 将当前数组分为左右两部分 递归,分别排序左右两部分 合并左右两部分,比较两堆中最上边的元素,哪个小就放入合并序列 | 最好:Θ(n) 平均:Θ(nlogn) 最差:Θ(nlogn) 空间:Θ(n) | 稳定 非就地排序 比较排序 分治递归 | 动态开辟空间、释放空间 递归终止条件(p<=r) 哨兵元素 函数输入参数能否取到 中间元素加入分组 |
|
6 | 堆排序 | 将数组整理成堆 交换根节点与最末尾元素交换,堆长减1 在根节点重新生成堆 重复,直到堆长为1 | 最好:O(nlogn) 平均:O(nlogn) 最差:Θ(nlogn) 空间:O(1) | 不稳定 就地排序 比较排序 | 左右孩子下标 下标是否超过堆长检查 建树起始下标 Max_heapify函数递归 |
|
7 | 快速排序 | 选择基准元(最后元素) 小于基准元的元素交换到前方 将基准元与小于其的元素的后一个交换,实现大于基准元的元素都在后方 分为两部分,递归调用 | 最好:Θ(n2) 平均:Θ(nlogn) 最差:Θ(nlogn) 空间:O(1) | 不稳定 就地排序 比较排序 分治 | 基准元本身不加入本组 函数输入参数能否取到 小于基准源元素起始下标 基准元交换元素下标 | 随机快速排序:随机选择基准元 三数取中,选取基准元 尾递归:用控制结构进行递归,而不是直接调用两次函数 |
8 | 计数排序 | 假设元素都小于等于k(或找到最大值) 开辟数组,记录各个元素出现的次数 从最前边,两两相加,计算小于等于元素的个数 从后向前遍历输入数组(保证稳定性),通过以上信息,直接将元素放入输出数组 更新小于等于元素的个数 | 最好:O(n+k) 平均:O(n+k) 最差:O(n+k) 空间:O(n+k) | 稳定 非就地排序 非比较排序 线性时间 | 小于等于元素个数为1的数,放在第一个,下标为0 动态开辟数组大小k+1 下标越界 对输入有要求 |
|
9 | 基数排序 | 一位一位的进行排序(k) 从低位向高位 子排序算法需要稳定(例如计数排序) | 最好:O(kn) 平均:O(kn) 最差:O(kn) 空间:O(kn)
| 稳定 非就地排序 非比较排序 线性时间 | 分割数字算法 |
|
10 | 桶排序 | 输入为[0,1]上的均匀分布 将[0,1]分为n个桶,将输入序列分到各个桶中 对每个桶进行插入排序 合并各个桶的结果 | 最好: 平均:O(n+k) 最差:O(n2) 空间:O(n*K) | 稳定 非就地排序 非比较排序 线性时间 |
|
|