1.拓扑排序是将有向图中所有结点排成一个线性序列,虽然也是在内存中进行的,但它不属于这里所提到的内部排序范畴,也不满足前面排序的定义。
2.对于任意序列进行基于比较的排序,求最少的比较次数应考虑最坏情况。对任意n个关键字排序的比较次数至少为 ⌈ l o g 2 ( n ! ) ⌉ \lceil log_2(n!) \rceil ⌈log2(n!)⌉
对任意7个关键字进行基于比较的排序,至少要进行几次关健字之间的两两比较。
13次
- 将 L ( i ) L (i) L(i) 放入哨兵中
- 前半部分有序表中,从后向前逐一与哨兵比较,查找 L ( i − 1 ) L(i-1) L(i−1)有序表中第一个<= L ( i ) L(i) L(i)的元素位置 L ( k ) L (k) L(k).
- 比较过程中将大于哨兵的元素向后移动一位
- 将 L ( i ) L (i) L(i) 复制到 L ( k + 1 ) L (k+1) L(k+1)中
- 算法从后向前寻找第一个小于等于自己的数即为停止条件
- 若 L ( i ) L (i) L(i) 小于前半部分有序序列,则会与哨兵即自己比较,从而确定应当插入表头位置
- 算法边比较边移动元素
#include
void insertsort(int a[],int n){
int i,j;
for(i=2;i<=n;i++){
/* 加入判断可减少进入循环的次数 */
if(a[i]<a[i-1]){
a[0] = a[i];
for (j = i-1;a[0]<a[j];j--){
a[j+1] = a[j];
}
/* 此时j 指向位置<=a[0],为插入位置前一位置,所以需要+1*/
a[j+1] = a[0];
}
}
}
void print_a(int a[],int n){
for(int i = 1;i<=n;i++){
printf(" %d ",*(a+i));
}
printf("\n");
}
int main(){
//指针地址为数组首地址
int *a;
int n;
printf("please input n:\n");
scanf("%d",&n);
printf("please input number:\n");
for(int i = 1;i<=n;i++){
scanf("%d",(a+i));
}
print_a(a,n);
insertsort(a,n);
print_a(a,n);
return 0;
}
- 空间复杂度: O ( 1 ) O (1) O(1)
- 进行 n n n-1趟,比较次数与移动次数取决于表的初始状态
- 时间复杂度:
1)最好 (有序): O ( n ) O (n) O(n) 。(只比较,不移动元素)
2 ) 最坏(逆序): O ( n 2 ) O (n^2) O(n2) 。(比较次数 ∑ i = 2 n i \sum\limits_{i=2}^{n}i i=2∑ni, 移动次数 ∑ i = 2 n ( i + 1 ) \sum\limits_{i=2}^{n}(i+1) i=2∑n(i+1))
3)平均: O ( n 2 ) O (n^2) O(n2)- 稳定性:稳定
- 将 L ( i ) L (i) L(i) 放入哨兵中
- 通过折半查找的方法,确定 L ( i ) L (i) L(i)在有序表 L ( i − 1 ) L (i-1) L(i−1)中的位置 L ( k ) L (k) L(k)
- 统一将 L ( k ) L (k) L(k)~ L ( i − 1 ) L (i-1) L(i−1) 向后移动
- 将 L ( i ) L (i) L(i) 复制到 L ( k ) L (k) L(k)中
- 折半法出现 h i g h < l o w high
high<low 的情况即为: h i g h high high位置的值小于关键字, l o w low low位置的值大于关键字,所以, l o w low low的位置(即 h i g h + 1 high+1 high+1)为插入元素位置。- 统一将 l o w low low~ L ( i − 1 ) L(i-1) L(i−1) 向后移动一位。
- 与直接插入相比,仅减少了比较元素次数。
- 数据量小的排序表,折半插入往往能表现出很好的性能。
void br_insertsort(int a[],int n){
int low ,high ,mid ;
for(int i = 2;i<=n;i++){
low = 1;
high = i-1;
a[0] = a[i];
while(low<=high){
mid = (low+high)/2;
if(a[mid]<a[0]) {low = mid+1;}
else high = mid-1;
}
for(int j=i-1;j>=low;j--){
a[j+1] = a[j];
}
a[low] = a[0];
}
}
- 空间复杂度: O ( 1 ) O (1) O(1)
- 时间复杂度: O ( n 2 ) O (n^2) O(n2)
- 比较元素次数: O ( n l o g 2 n ) O (nlog_2n) O(nlog2n) 与排序表初始状态无关,仅取决于元素个数 n n n
- 移动次数:(与直接插入相同) 依赖排序表的初始状态
- 稳定性:稳定
- 取步长 d k 1 dk1 dk1 (一般为表长的一半 n / 2 n/2 n/2), 将所有距离为 d k 1 dk1 dk1的倍数的记录分为一组
- 对各组进行直接插入排序(此时,每个元素的前一比较位置为 i − d k 1 i-dk1 i−dk1)
- 取步长 d k 2 dk2 dk2( d k 2 = d k 1 / 2 dk2 = dk1/2 dk2=dk1/2), 继续进行直接插入排序。直到 d k = 1 dk=1 dk=1,排序完成。
void shellsort(int a[], int n){
for(int dk = n/2;dk>=1;dk = dk/2){
int i = 1;
int j;
//直接插入排序思想
for(i =i+dk;i<=n;i++){
if(a[i]<a[i-dk]){
a[0] = a[i];
for(j = i-dk;j>0&&a[j]>a[0];j = j-dk){
a[j+dk] = a[j];
}
a[j+dk] = a[0];
}
}
}
}
- 空间复杂度: O ( 1 ) O (1) O(1)
- 时间复杂度:最坏: O ( n 2 ) O (n^2) O(n2) (复杂度依赖于增量序列的函数)
- 稳定性:不稳定
- 仅适用于线性表为顺序存储的情况
对有n个元素的顺序表采用直接插入排序算法进行排序,在最坏情况下所需的比较次数是();在最好情况下所需的比较次数是()。
待排序表为反序时,直接插入排序需要进行 n ( n − 1 ) / 2 n(n-1)/2 n(n−1)/2 次比较;待排序表为正序时,只需进行 n − 1 n-1 n−1 次比较。
- 从后向前,两两比较元素大小,若 L ( j ) < L ( j − 1 ) L(j) < L(j-1) L(j)<L(j−1) 交换两元素,将 j j j 减1 继续比较 L ( j ) L(j) L(j) 与 L ( j − 1 ) L(j-1) L(j−1)。
- 第一趟结束后,将最小元素放在最前,位置确定,下一趟不参与比较(外层循环结束判定条件 j > i j > i j>i, i i i 随趟数增加而增加),进行第二趟排序。
- 在循环中设置标志,每趟排序判断是否发生交换元素,若未发生则排序完成。
- 每趟排序都会将一个元素放在最终位置
- 冒泡排序最多做 n − 1 n-1 n−1 趟
void bubble_sort(int a[],int n){
int flag = 0,temp;
for(int i=1;i<=n;i++){
//此处判别条件为j>i,
//每趟确定一个位置元素,所以不用重复比较
for(int j=n;j>i;j--){
if(a[j]<a[j-1]){
temp = a[j];
a[j] = a[j-1];
a[j-1] = temp;
flag = 1;
}
}
//return 直接退出函数
if(flag == 0) return ;
}
}
- 空间复杂度: O ( 1 ) O (1) O(1)
- 时间复杂度:
1)最好 (有序): O ( n ) O (n) O(n) 。(比较 n − 1 n-1 n−1 次,移动0次)
2 ) 最坏(逆序): O ( n 2 ) O (n^2) O(n2) 。( n − 1 n-1 n−1 趟排序)
3)平均: O ( n 2 ) O (n^2) O(n2)- 稳定性:稳定
- 传入 a [ ] a[ ] a[],表的首位,末位。判断 l o w < h i g h ? low < high ? low<high?
- 选择表中第一个元素作为基准( a [ l o w ] a[ low ] a[low]),放入 a [ 0 ] a[ 0 ] a[0]中
- 从 a [ h i g h ] a[ high ] a[high]从后向前寻找第一个小于基准的值,放入 a [ l o w ] a[ low ] a[low],然后 a [ l o w ] a[ low ] a[low]从前向后寻找第一个大于基准的值,将更新后的 a [ h i g h ] a[ high ] a[high]放入,交替进行,直到 l o w = h i g h low = high low=high
- 返回基准插入位置,将表分为两个子表,进行下一趟排序。
- 借助递归栈完成每趟排序
- 递归调用时,首先判断 l o w < h i g h ? low < high ? low<high?,此为递归跳出条件( l o w = h i g h low = high low=high时,说明表中只有一个元素,子表有序,跳出递归)
- 快速排序是对冒泡排序的一种改进,基于分治的思想
- 每趟确定子表基准元素位置。
- 性能主要取决于划分操作的好坏。
- 提高算法效率:
1) 子序列规模小时,采用直接插入排序
2) 选一个可将数据平分的基准- 快速排序是所有内部排序算法中平均性能最优的
int partition(int a[], int low, int high){
//首先将基准放入a[0]中
//每次运行确定一个基准元素
a[0] = a[low];
while(low<high){
//从后向前寻找high小于基准元素时,也需判断high与low的关系
while(low<high&&a[high]>=a[0]){ high--;}
a[low] = a[high];
while(low<high&&a[low]<=a[0]) {low++;}
a[high] = a[low];
}
a[low] = a[0];
return low;
}
void quick_sort(int a[],int low, int high){
if(low<high){//递归跳出条件
int pos = partition(a,low,high);
quick_sort(a,low,pos-1);
quick_sort(a,pos+1,high);
}
}
- 空间复杂度: 容量与递归调用的最大深度一直
1)最好 : ⌊ l o g 2 ( n + 1 ) ⌋ \lfloor log_2(n+1)\rfloor ⌊log2(n+1)⌋ 。
2 ) 最坏: O ( n ) O (n) O(n) 。( n − 1 n-1 n−1 次递归)
3)平均: O ( l o g 2 n ) O (log_2n) O(log2n)- 时间复杂度:运行时间与划分是否对称有关
1)最好 (平均划分): O ( n l o g 2 n ) O (nlog_2n) O(nlog2n) 。
2 ) 最坏(基本有序或逆序): O ( n 2 ) O (n^2) O(n2) 。
3)平均: O ( n l o g 2 n ) O (nlog_2n) O(nlog2n)- 稳定性:不稳定
选择排序:元素比较次数与序列的初始状态无关始终是 n ( n − 1 ) / 2 n(n-1)/2 n(n−1)/2
- 第 i i i趟排序,即从 L ( i ) L(i) L(i)~ L ( n ) L(n) L(n)中选出最小元素
- 设置 m i n min min,判断 L ( i ) L(i) L(i)是否等于 L ( m i n ) L(min) L(min), 若 L ( m i n ) < L ( i ) L(min) < L(i) L(min)<L(i),交换两元素值
- 继续选择 L ( i + 1 ) L(i+1) L(i+1) ~ L ( n ) L(n) L(n)中最小元素,放在 L ( i + 1 ) L(i+1) L(i+1)处
- 每趟排序都会将一个元素放在最终位置
- 经过 n − 1 n-1 n−1 趟排序就可使得排序表有序
void sim_selesort(int a[], int n){
for(int i=1;i<=n;i++){
int min = i,temp;
for(int j=n;j>i;j--){
if(a[min]>a[j]){
min = j;
}
}
if(min!=i){
temp = a[i];
a[i] = a[min];
a[min] = temp;
}
}
}
- 空间复杂度: O ( 1 ) O (1) O(1)
- 时间复杂度: O ( n 2 ) O (n^2) O(n2) (元素比较次数与序列的初始状态无关始终是 n ( n − 1 ) / 2 n(n-1)/2 n(n−1)/2)
- 稳定性:不稳定
- 设计建堆函数(从 n / 2 n/2 n/2开始,递减,使双亲大于孩子结点),设计向下调整函数,判断双亲结点与子孙结点大小(从当前双亲结点判断到表尾,每次步长 i ∗ 2 i*2 i∗2,将子树中最大值放在双亲结点)
- 排序过程:先建堆,然后交换首尾元素(将最大值放在表尾),利用向下调整函数将 1 1 1~ n − 1 n - 1 n−1中最大值放在表首,交换首尾元素
- 排序过程,堆大小每次减一,最大值放在最后
- 堆常被用来实现优先级队列
- 删除堆顶元素时,堆顶元素与最后一个元素互换,再从根结点自上向下调整
- 插入元素时,将新节点放在堆尾,对这个新结点执行向上调整操作
#include
//将第一元素调整为最大元素函数
//1.获得参数为,数组,双亲结点位置,表长
//2.将双亲结点给a[0],以便将来交换双亲与孩子结点值
//3.向下循环寻找大于双亲结点的孩子结点,每次步长为:双亲结点*2
//4.若碰到 双亲结点 > 孩子结点 则退出循环
// 原因1.建堆时,函数是从最后一个双亲结点开始判断,建的初始堆满足双亲结点大于孩子结点
// 2. heapsort函数中调用时,初始堆满足上大下小,不用担心有孙子结点大于孩子结点的情况
void adjustdown(int a[],int k,int n){
a[0] = a[k];
for(int i=2*k;i<=n;i=i*2){
if(i<n&&a[i]<a[i+1]){i++;}
if(a[0]>=a[i]) break;
else {
a[k] = a[i]; // 孩子结点的值给双亲
k = i; //k 指向孩子结点,方便赋值
}
}
a[k] = a[0]; //找到k 的最终位置后,将a[0]的值给a[k]
}
//建立大顶堆,从n/2开始向前比较双亲结点,使第一个元素为最大元素
void buildmaxheap(int a[],int n){
for(int i = n/2;i>0;i--){
adjustdown(a,i,n);
}
}
//1.先建立大顶堆(使第一个元素最大)
//2.交换第一个与最后一个元素
//3.在1~n-1个元素中找最大值(调用函数),继续交换首尾元素
//4.最终得到从小到大排序表
void heapsort(int a[],int n){
buildmaxheap(a,n);
for(int i=n;i>1;i--){
int temp;
temp = a[i];
a[i] = a[1];
a[1] = temp;
adjustdown(a,1,i-1);//i = n;所以调整元素为1~i-1
}
}
//向上调整操作(插入元素,插入堆尾)
void adjustup(int a[],int k){
a[0] = a[k];
for(int i = k/2;i>0;i=i/2){
if(a[0]>a[i]){
a[k] = a[i];
k = i;
}
}
a[k] = a[0];
}
- 空间复杂度: O ( 1 ) O (1) O(1)
- 时间复杂度(最好、最坏、平均): O ( n l o g 2 n ) O (nlog_2n) O(nlog2n)(建堆时间 O ( n ) O( n ) O(n), n − 1 n-1 n−1次向下调整操作,每次时间复杂度为 O ( h ) O(h) O(h))
- 稳定性:不稳定
向具有n个结点的堆中插入一个新元素的时间复杂度为(),删除一个元素的时间复杂度为().
在向有 n n n个元素的堆中插入一个新元素时,需要调用一个向上调整的算法,比较次数最多等于树的高度减1,由于树的高度为 ⌊ l o g 2 n ⌋ + 1 \lfloor log_2n \rfloor+1 ⌊log2n⌋+1,所以堆的向上调整算法的比较次数最多等于 ⌊ l o g 2 n ⌋ \lfloor log_2n \rfloor ⌊log2n⌋。
此处需要注意,调整堆和建初始堆的时间复杂度是不一样的
归并排序与序列的初始状态无关
- 设计递归函数(找到两个最小合并表,逐层向上合并)。设计合并两表函数(有序表的合并)
- 有序表的合并:待排序两表相邻,传入两表总长的首末位置( l o w , h i g h low, high low,high),传入第二个表首位置( m i d = ( l o w + h i g h ) / 2 mid = (low+high)/2 mid=(low+high)/2),借助数组,判断值大小,完成合并。
- 递归函数中传入表的首末位置( l o w , h i g h low, high low,high),调用函数首先判断( l o w < h i g h low < high low<high递归退出条件),然后将表分为两份,递归调用两子表,整个算法思想:从子表开始两两合并,最终得到排序表。
- 递归形式的2路归并排序基于分治思想
- 归并排序与序列的初始状态无关
#include
//实现两表的归并
void merge(int a[],int low, int mid, int high,int b[]){
int t= 0;
int i = low,j = mid+1;
while(i<=mid&&j<=high){
if(a[i]<=a[j]){ b[t++] = a[i++];}
else {b[t++] = a[j++];}
}
while(i<=mid){b[t++] = a[i++];}
while(j<=high){b[t++] = a[j++];}
for(int k=0;k<t;k++){ a[low+k] = b[k];}
}
//递归调用函数归并两表
void merge_sort(int a[],int low ,int high,int b[]){
if(low<high){//递归退出条件
int mid = (low+high)/2; //划分两个子表
merge_sort(a,low,mid,b);//对第一个子表进行归并排序
merge_sort(a,mid+1,high,b);
merge(a,low,mid,high,b) ;
}
}
void print_a(int a[], int n){
for(int i = 0;i<n;i++){
printf(" %d ",*(a+i));
}
printf("\n");
}
int main(){
int a[5] = {3,5,4,1,2};
int b[5];
print_a(a,5);
merge_sort(a,0,4,b);
print_a(a,5);
return 0;
}
- 空间复杂度: O ( n ) O (n) O(n)( m e r g e ( ) merge() merge()中占 n n n个辅助空间)
- 时间复杂度: O ( n l o g 2 n ) O (nlog_2n) O(nlog2n)(每趟归并 O ( n ) O(n) O(n),共进行 ⌈ l o g 2 n ⌉ \lceil log_2n \rceil ⌈log2n⌉ 趟归并)
- 稳定性:稳定
对于 N N N个元素进行 k k k路归并排序时,排序的趟数 m m m满足 k m = N k^m=N km=N,从而 m = l o g k N m=log_kN m=logkN,又考虑到 m m m为整数,所以 m = ⌈ l o g k N ⌉ m=\lceil log_kN\rceil m=⌈logkN⌉.这和前面的2路归并是一致的。
基数排序不能对 f l o a t , d o u b l e float, double float,double类型的实数进行排序
- 空间复杂度: O ( r ) O (r) O(r) (一趟排序需要辅助空间为 r r r个队列)
- 时间复杂度: O ( d ( n + r ) ) O (d(n+r)) O(d(n+r)) 与序列的初始状态无关(需要进行 d d d趟分配与收集,一趟分配需要 O ( n ) O (n) O(n),一趟收集需要 O ( r ) O(r) O(r))
- 稳定性:稳定
将两个各有N个元素的有序表合并成一个有序表,最少的比较次数是(),最多的比较次数是()。
N N N, 2 N − 1 2N-1 2N−1
当一个表中的最小元素比另一个表中的最大元素还大时,比较的次数是最少的,仅比较 N N N次;而当两个表中的元素依次间隔地比较时,即 a 1 < b 1 < a 2 < b 2 < … < a n < b a1a1<b1<a2<b2<…<an<b 。时,比较的次数是最多的,为 2 N − 1 2N-1 2N−1次。
- 快速排序被认为是目前基于比较的内部排序法中最好的方法。
- 若文件的初始状态已按关键字基本有序,则选用直接插入或冒泡排序为宜。
- 当文件的 n n n个关键字随机分布时,任何借助于“比较”的排序算法,至少需要 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)的时间。
- 若 n n n较小 ( n < = 50 ) (n<=50) (n<=50),则可采用直接插入排序或简单选择排序。
- 若 n n n较大,则应采用时间复杂度为 O ( n l o g 2 n ) O(nlog_2n) O(nlog2n)的排序方法:快速排序、堆排序或归并排序。
- 若 n n n很大,记录的关键字位数较少且可以分解时,采用基数排序较好。
排序趟数与序列的原始状态无关的排序方法是()。
I.直接插入排序 Ⅱ.简单选择排序 Ⅲ.冒泡排序 IV.基数排序交换类的排序,其趟数和原始序列状态有关,故冒泡排序与初始序列有关。直接插入排序:每趟排序都插入一个元素,所以排序趟数固定为 n − 1 n-1 n−1;简单选择排序:每趟排序都选出一个最小(或最大)的元素,所以排序趟数固定为 n − 1 n-1 n−1;基数排序:每趟排序都要进行“分配”和“收集”,排序趟数固定为 d d d。