版权声明: https://blog.csdn.net/weixin_42061048/article/details/82051017
11_排序算法
标签(空格分隔): 数据结构和算法
- 排序算法
- 排序的基本概念与分类
- 排序的稳定性
- 影响排序算法性能的几个要素
- 冒泡排序
- 选择排序
- 直接插入排序
- 希尔排序
- 堆排序
- 归并排序
- 快速排序
11.1 排序算法
11.1.1 排序的基本概念与分类
概念
- 假设含有n个记录的序列为{r1, r2, …, rn},其相应的关键字分别为{k1, k2, …, kn},需确定1, 2, …, n 的一种排列 p1, p2, …, pn,使其相应的关键字满足 Kp1<=Kp2<=..<=Kpn 非递减(或非递增)关系,即使得序列成为一个按关键字有序的序列{rp1, rp2, …, rpn},这样的操作就称为排序。
在排序问题中,通常将数据元素称为记录。
- 显然我们输入的是一个记录集合,排序后输出的也是一个记录集合。
- 所以我们可以将排序看成是线性表的一种操作。
排序的依据是关键字之间的大小关系,那么对同一记录集合,针对不同的关键字进行排序,可以得到不同的序列。
分类
插入排序类
- 直接插入排序
- 希尔排序
选择排序类
- 简单选择排序
- 堆排序
交换排序类
- 冒泡排序
- 快速排序
归并排序类
- 归并排序
11.1.2 排序的稳定性
- 假设ki=kj(1<=i<=n, 1<=j<=n, i!=j),且在排序前的序列中ri领先于rj(即i
11.1.3 影响排序算法性能的几个要素
- 时间性能
- 辅助空间
- 算法的复杂性
11.2 冒泡排序
冒泡排序的要点
- 两两注意是相邻的两个元素的意思
- 如果有n个元素需要比较n-1次,每一轮减少1次比较
- 既然叫冒泡排序,那就是从下往上两两比较,所以看上去就跟泡泡往上冒一样
代码实现
#include <stdio.h>
void BubbleSort(int k[], int n)
{
int i, j, temp, count1=0, count2=0;
for( i=0; i < n-1; i++ )
{
for( j=i+1; j < n; j++ )
{
count1++;
if( k[i] > k[j] )
{
count2++;
temp = k[j];
k[j] = k[i];
k[i] = temp;
}
}
}
printf("总共进行了%d次比较,进行了%d次移动!\n", count1, count2);
}
int main()
{
int i;
int a[10] = {5, 2, 6, 0, 3, 9, 1, 7, 4, 8};
BubbleSort(a, 10);
printf("排序后的结果是: \n");
for( i=0; i < 10; i++ )
{
printf("%d", a[i]);
}
printf("\n");
return 0;
}
- 优化版
#include <stdio.h>
void BubbleSort(int k[], int n)
{
int i, j, temp, count1=0, count2=0;
int flag = 1;
for( i=0; i < n-1 && flag; i++ )
{
for( j=n-1; j > i; j-- )
{
count1++;
flag = 0;
if( k[j-1] > k[j] )
{
count2++;
temp = k[j-1];
k[j-1] = k[j];
k[j] = temp;
flag = 1;
}
}
}
printf("总共进行了%d次比较,进行了%d次移动!\n", count1, count2);
}
int main()
{
int i;
int a[10] = {5, 2, 6, 0, 3, 9, 1, 7, 4, 8};
BubbleSort(a, 10);
printf("排序后的结果是: \n");
for( i=0; i < 10; i++ )
{
printf("%d", a[i]);
}
printf("\n");
return 0;
}
11.3 选择排序
选择排序算法就是通过n-i次关键字间的比较,从n-i+1个记录中最小的记录,并和第i(1<=i<=n)个记录交换。
代码实现
#include <stdio.h>
void SelectSort(int k[], int n)
{
int i, j, min, temp, count1=0, count2=0;
for( i=0; i < n-1; i++ )
{
min = i;
for( j=i+1; j < n; j++ )
{
count1++;
if( k[j] < k[min] )
{
min = j;
}
}
if( min != i )
{
count2++;
temp = k[min];
k[min] = k[i];
k[i] = temp;
}
}
printf("总共进行了%d次比较,进行了%d次移动!\n", count1, count2);
}
int main()
{
int i;
int a[10] = {5, 2, 6, 0, 3, 9, 1, 7, 4, 8};
SelectSort(a, 10);
printf("排序后的结果是: \n");
for( i=0; i < 10; i++ )
{
printf("%d", a[i]);
}
printf("\n");
return 0;
}
11.4 直接插入排序
直接插入排序算法(Straight Insertion Sort)的基本操作是将一个记录插入到已经排好序的有序表中,从而得到一个新的、记录数增加1的有序表。
代码实现
#include <stdio.h>
void InsertSort(int k[], int n)
{
int i, j, temp;
for( i=1; i < n; i++ )
{
if( k[i] < k[i-1] )
{
temp = k[i];
for( j=i-1; k[j] > temp; j-- )
{
k[j+1] = k[j];
}
k[j+1] = temp;
}
}
}
int main()
{
int i;
int a[10] = {5, 2, 6, 0, 3, 9, 1, 7, 4, 8};
InsertSort(a, 10);
printf("排序后的结果是: \n");
for( i=0; i < 10; i++ )
{
printf("%d", a[i]);
}
printf("\n");
return 0;
}
11.5 希尔排序
- 一般排序算法的时间复杂度为O(n^2),希尔排序的时间复杂度为O(n*log n)。
初始关键字 | 49 | 38 | 65 | 97 | 76 | 13 | 27 | 49 | 55 | 04 |
---|---|---|---|---|---|---|---|---|---|---|
一趟排序结果 | 13 | 27 | 49 | 55 | 04 | 49 | 38 | 65 | 97 | 76 |
二趟排序结果 | 13 | 04 | 49 | 38 | 27 | 49 | 55 | 65 | 97 | 76 |
三趟排序结果 | 04 | 13 | 27 | 38 | 49 | 49 | 55 | 65 | 76 | 97 |
- 代码实现
#include <stdio.h>
void ShellSort(int k[], int n)
{
int i, j, temp;
int gap = n;
do
{
gap /= 3;
for( i=gap; i < n; i++ )
{
if( k[i] < k[i-gap] )
{
temp = k[i];
for( j=i-gap; k[j] > temp; j-=gap )
{
k[j+gap] = k[j];
}
k[j+gap] = temp;
}
}
} while(gap > 0);
}
int main()
{
int i;
int a[10] = {5, 2, 6, 0, 3, 9, 1, 7, 4, 8};
ShellSort(a, 10);
printf("排序后的结果是: \n");
for( i=0; i < 10; i++ )
{
printf("%d", a[i]);
}
printf("\n");
return 0;
}
11.6 堆排序
堆,是具有以下性质的完全二叉树
- 每个结点的值都大于或等于其左右孩子的结点的值,称为大顶堆。
- 每个结点的值都小于或等于其左右孩子的结点的值,称为小顶堆。
要点
根结点一定是堆中所有结点最大或者最小者,如果按照层序遍历的方式给结点从1开始编号,则结点之间满足如下关系:
- Ki >= K2i 且 Ki >= K2i+1
- 或 Ki <= K2i 且 Ki <= K2i+1
- (1<=i<=|n/2|)
下标i与2i和2i+1是双亲和子女关系
- 那么把大顶堆和小顶堆用层序遍历存入数组,则一定满足上面的表达式。
堆排序(Heap Sort)就是利用堆进行排序的算法,它的基本思想是:
- 将待排序的序列构造成一个大顶堆(或小顶堆)。
- 此时,整个序列的最大值就是堆顶的根结点。将它移走(就是将其与堆数组的末尾元素交换,此时末尾元素就是最大值)。
- 然后将剩余的n-1个序列重新构造成一个堆,这样就会得到n个元素中的最大值。
- 如此反复执行,便能得到一个有序序列了。
代码实现
#include <stdio.h>
int count = 0;
void swap(int k[], int i, int j)
{
int temp;
temp = k[i];
k[i] = k[j];
k[j] = temp;
}
void HeapAdjust(int k[], int s, int n)
{
int i, temp;
temp = k[s];
// i=2*s:左孩子; 2*s+1: 右孩子
for( i=2*s; i <= n; i*=2 )
{
count++;
if( i < n && k[i] < k[i+1] ) // 左孩子小于右孩子
{
i++; // 使得i指向最大的孩子的下标
}
if( temp >= k[i] ) // temp临时存放当前需要调整的双亲。如果大于孩子,则退出循环
{
break;
}
k[s] = k[i];
s = i;
}
k[s] = temp;
}
void HeapSort(int k[], int n)
{
int i;
// 从下至上,从右至左,层序遍历
for( i=n/2; i > 0; i-- )
{
HeapAdjust(k, i, n); // 构建大顶堆的函数。 k:数组 i:当前双亲结点; n:长度
}
for( i=n; i > 1; i-- )
{
swap(k, 1, i); // 调整,将第一个元素和最后一个元素进行互换,i是变化的
HeapAdjust(k, 1, i-1); // 重新调整
}
}
int main()
{
int i;
int a[10] = {-1, 5, 2, 6, 0, 3, 9, 1, 7, 4};
HeapSort(a, 9);
printf("总共执行 %d 次比较! \n", count);
printf("排序后的结果是: ");
for( i=1; i < 10; i++ )
{
printf("%d ", a[i]);
}
printf("\n");
return 0;
}
11.7 归并排序
归并排序(Merge Sort)就是利用归并的思想实现的排序方法。它的原理是假设初始有n个记录,则可以看成是n个有序的子序列,每个子序列的长度为1,然后两两归并,得到[n/2]个长度为2或1的有序子序列;然后再两两归并,……,如此重复,直至得到一个长度为n的有序序列为止,这种排序方法称为2路归并排序。
归并排序的递归实现
// 递归实现
#include <stdio.h>
#define MAXSIZE 10
//实现归并,并把最后的结果存放到list1数组
void merging(int *list1, int list1_size, int *list2, int list2_size)
{
int i, j, k, m;
int temp[MAXSIZE];
i = j = k = 0
while( i < list1_size && j < list2_size )
{
if( list1[i] < list2[j] )
{
temp[k++] = list1[i++];
}
else
{
temp[k++] = list2[j++];
}
}
while( i < list1_size )
{
temp[k++] = list1[i++];
}
while( j < list2_size )
{
temp[k++] = list2[j++];
}
for( m=0; m < (list1_size + list2_size); m++ )
{
list1[m] = temp[m];
}
}
void MergeSort(int k[], int n)
{
if( n > 1 )
{
int *list1 = k;
int list1_size = n/2;
int *list2 = k + n/2; // 左半部分的地址加上左半部分的尺寸
int list2_size = n - list1_size;
MergeSort(list1, list1_size);
MergeSort(list2, list2_size);
merging(list1, list1_size, list2, list2_size);
}
}
int main()
{
int i;
int a[10] = {5, 2, 6, 0, 3, 9, 1, 7, 4, 8};
MergeSort(a, 10);
printf("排序后的结果是: \n");
for( i=0; i < 10; i++ )
{
printf("%d", a[i]);
}
printf("\n");
return 0;
}
- 归并排序的迭代实现
// 迭代实现
#include <stdio.h>
#include <stdlib.h>
#define MAXSIZE 10
void MergeSort(int k[], int n)
{
int i, next, left_min, left_max, right_min, right_max;
// 申请空间存放数组
int *temp = (int *)malloc(n * sizeof(int));
// 逐级递增比较
for( i=1; i < n; i*=2 ) // 步长i
{
for( left_min=0; left_min < n-i; left_min = right_max )
{
right_min = left_max = left_min + i;
right_max = left_max + i;
// 右边的下标最大值只能为n,防止越界
if( right_max > n )
{
right_max = n;
}
// temp数组的下标,由于每次数据都有返回到k,因此每次都需重新置零
next = 0;
// 如果左边的数据还没有到分割线且右边的数据也没有到分割线,则循环
while( left_min < left_max && right_min < right_max )
{
if( k[left_min] < k[right_min] )
{
temp[next++] = k[left_min++]; // 存放较小者
}
else
{
temp[next++] = k[right_min++];
}
}
// 如果左边的游标没有到达分割线,则需要把数组接回去
// 如果右边的游标没有到达分割线,则说明右边的数据比较大,不需要移动位置
while( left_min < left_max )
{
k[--right_min] = k[--left_max];
}
while( next > 0 )
{
// 将排好序的数组返回给k
k[--right_min] = temp[--next];
}
}
}
}
int main()
{
int i;
int a[10] = {5, 2, 6, 0, 3, 9, 1, 7, 4, 8};
MergeSort(a, 10);
printf("排序后的结果是: \n");
for( i=0; i < 10; i++ )
{
printf("%d", a[i]);
}
printf("\n");
return 0;
}
11.8 快速排序
- 代码实现
#include <stdio.h>
void swap(int k[], int low, int high)
{
int temp;
temp = k[low];
k[low] = k[high];
k[high] = temp;
}
int Partition(int k[], int low, int high)
{
int point;
point = k[low];
while( low < high )
{
while( low < high && k[high] >= point )
{
high--;
}
swap(k, low, high);
while( low < high && k[low] <= point )
{
low++;
}
swap(k, low, high);
}
return low;
}
void QSort(int k[], int low, int high)
{
int point;
if( low < high )
{
point = Partition(k, low, high); // 计算基准点,将小于基准点的数放在左边,大的放在右边
QSort(k, low, point-1);
QSort(k, point+1, high);
}
}
void QuickSort(int k[], int n)
{
QSort(k, 0, n-1);
}
int main()
{
int i;
int a[10] = {5, 2, 6, 0, 3, 9, 1, 7, 4, 8};
QuickSort(a, 10);
printf("排序后的结果是: \n");
for( i=0; i < 10; i++ )
{
printf("%d", a[i]);
}
printf("\n");
return 0;
}
11.8.1 快速排序的优化
11.8.1.1 优化选取基准点
- 三数取中法
#include <stdio.h>
void swap(int k[], int low, int high)
{
int temp;
temp = k[low];
k[low] = k[high];
k[high] = temp;
}
int Partition(int k[], int low, int high)
{
int point;
int m = low + (high-low)/2;
if( k[low] > k[high] )
{
swap(k, low, high);
}
if( k[m] > k[high] )
{
swap(k, m, high);
}
if( k[m] > k[low] )
{
swap(k, m, low);
}
// 将low变成中间值
point = k[low];
while( low < high )
{
while( low < high && k[high] >= point )
{
high--;
}
swap(k, low, high);
while( low < high && k[low] <= point )
{
low++;
}
swap(k, low, high);
}
}
void QSort(int k[], int low, int high)
{
int point;
if( low < high )
{
point = Partition(k, low, high); // 计算基准点,将小于基准点的数放在左边,大的放在右边
QSort(k, low, point-1);
QSort(k, point+1, high);
}
}
void QuickSort(int k[], int n)
{
QSort(k, 0, n-1);
}
int main()
{
int i;
int a[10] = {5, 2, 6, 0, 3, 9, 1, 7, 4, 8};
QuickSort(a, 10);
printf("排序后的结果是: \n");
for( i=0; i < 10; i++ )
{
printf("%d", a[i]);
}
printf("\n");
return 0;
}
11.8.1.2 优化不必要的交换
#include <stdio.h>
void swap(int k[], int low, int high)
{
int temp;
temp = k[low];
k[low] = k[high];
k[high] = temp;
}
int Partition(int k[], int low, int high)
{
int point;
int m = low + (high-low)/2;
if( k[low] > k[high] )
{
swap(k, low, high);
}
if( k[m] > k[high] )
{
swap(k, m, high);
}
if( k[m] > k[low] )
{
swap(k, m, low);
}
// 将low变成中间值
point = k[low];
while( low < high )
{
while( low < high && k[high] >= point )
{
high--;
}
k[low] = k[high];
while( low < high && k[low] <= point )
{
low++;
}
k[high] = k[low];
}
k[low] = point;
}
void QSort(int k[], int low, int high)
{
int point;
if( low < high )
{
point = Partition(k, low, high); // 计算基准点,将小于基准点的数放在左边,大的放在右边
QSort(k, low, point-1);
QSort(k, point+1, high);
}
}
void QuickSort(int k[], int n)
{
QSort(k, 0, n-1);
}
int main()
{
int i;
int a[10] = {5, 2, 6, 0, 3, 9, 1, 7, 4, 8};
QuickSort(a, 10);
printf("排序后的结果是: \n");
for( i=0; i < 10; i++ )
{
printf("%d", a[i]);
}
printf("\n");
return 0;
}
11.8.1.3 优化小数组时的排序方案
#include <stdio.h>
#define MAX_LENGTH_INSERT_SORT 7
void ISort(int k[], int n)
{
int i, j, temp;
for( i=1; i < n; i++ )
{
if( k[i] < k[i-1] )
{
temp = k[i];
for( j=i-1; k[j] > temp; j-- )
{
k[j+1] = k[j];
}
k[j+1] = temp;
}
}
}
void InsertSort(int k[], int low, int high)
{
ISort(k+low, high-low+1);
}
void swap(int k[], int low, int high)
{
int temp;
temp = k[low];
k[low] = k[high];
k[high] = temp;
}
int Partition(int k[], int low, int high)
{
int point;
int m = low + (high-low)/2;
if( k[low] > k[high] )
{
swap(k, low, high);
}
if( k[m] > k[high] )
{
swap(k, m, high);
}
if( k[m] > k[low] )
{
swap(k, m, low);
}
// 将low变成中间值
point = k[low];
while( low < high )
{
while( low < high && k[high] >= point )
{
high--;
}
k[low] = k[high];
while( low < high && k[low] <= point )
{
low++;
}
k[high] = k[low];
}
k[low] = point;
}
void QSort(int k[], int low, int high)
{
int point;
if( high - low > MAX_LENGTH_INSERT_SORT )
{
point = Partition(k, low, high); // 计算基准点,将小于基准点的数放在左边,大的放在右边
QSort(k, low, point-1);
QSort(k, point+1, high);
}
else
{
InsertSort(k, low, high);
}
}
void QuickSort(int k[], int n)
{
QSort(k, 0, n-1);
}
int main()
{
int i;
int a[10] = {5, 2, 6, 0, 3, 9, 1, 7, 4, 8};
QuickSort(a, 10);
printf("排序后的结果是: \n");
for( i=0; i < 10; i++ )
{
printf("%d", a[i]);
}
printf("\n");
return 0;
}
11.8.1.4 优化递归操作
- 尾递归
- 如果一个函数中递归形式的调用出现在函数的末尾,我们称这个递归函数是尾递归的。
- 当编译器检测到一个函数调用是尾递归的时候,它就覆盖当前的活跃记录,而不是在栈中去创建一个新的。编译器可以做到这点,因为递归调用是当前活跃期内最后一条待执行的语句,于是当这个调用返回时栈帧中并没有其他事情可以做,因此也就没有保存栈帧的必要了。通过覆盖当前的栈帧而不是在其之上重新添加一个,这样所使用的栈空间就大大缩减了,这使得实际运行效率会变得更高。
- 因此,只要有可能,我们就需要将函数携程
#include <stdio.h>
#define MAX_LENGTH_INSERT_SORT 7
void ISort(int k[], int n)
{
int i, j, temp;
for( i=1; i < n; i++ )
{
if( k[i] < k[i-1] )
{
temp = k[i];
for( j=i-1; k[j] > temp; j-- )
{
k[j+1] = k[j];
}
k[j+1] = temp;
}
}
}
void InsertSort(int k[], int low, int high)
{
ISort(k+low, high-low+1);
}
void swap(int k[], int low, int high)
{
int temp;
temp = k[low];
k[low] = k[high];
k[high] = temp;
}
int Partition(int k[], int low, int high)
{
int point;
int m = low + (high-low)/2;
if( k[low] > k[high] )
{
swap(k, low, high);
}
if( k[m] > k[high] )
{
swap(k, m, high);
}
if( k[m] > k[low] )
{
swap(k, m, low);
}
// 将low变成中间值
point = k[low];
while( low < high )
{
while( low < high && k[high] >= point )
{
high--;
}
k[low] = k[high];
while( low < high && k[low] <= point )
{
low++;
}
k[high] = k[low];
}
k[low] = point;
}
void QSort(int k[], int low, int high)
{
int point;
if( high - low > MAX_LENGTH_INSERT_SORT )
{
while( low < high )
{
point = Partition(k, low, high); // 计算基准点,将小于基准点的数放在左边,大的放在右边
if( point-low < high-point )
{
QSort(k, low, point-1);
low = point + 1;
}
else
{
QSort(k, point+1, high);
high = point-1;
}
}
}
else
{
InsertSort(k, low, high);
}
}
void QuickSort(int k[], int n)
{
QSort(k, 0, n-1);
}
int main()
{
int i;
int a[10] = {5, 2, 6, 0, 3, 9, 1, 7, 4, 8};
QuickSort(a, 10);
printf("排序后的结果是: \n");
for( i=0; i < 10; i++ )
{
printf("%d", a[i]);
}
printf("\n");
return 0;
}
11.9 总结回顾
排序方法 | 平均情况 | 最好情况 | 最坏情况 | 辅助空间 | 稳定性 |
---|---|---|---|---|---|
冒泡排序 | 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(nlogn)~O(n) | 不稳定 |