数据结构 第八章 排序
来源:互联网 发布:青山大禹软件 编辑:程序博客网 时间:2024/05/29 02:25
数据结构系列笔记链接:
本文同步发布于我的个人网站:数据结构教程
第一章 绪论
第二章 线性表
第三章 栈和队列
第四章 串
第五章 数组和广义表
第六章 树和二叉树
第七章 图
第八章 排序
第九章 查找
8 排序
8.1 基本概念
有n个记录的序列{R1,R2,…,Rn}
,其相应关键字的序列是{K1,K2, …,Kn }
,相应的下标序列为1,2,…,n
。
通过排序,要求找出当前下标序列1,2,…, n
的一种排列p1,p2, …,pn
,使得相应关键字满足如下的非递减(或非递增)关系,即:Kp1≤ Kp2≤…≤ Kpn
,这样就得到一个按关键字有序的记录序列:{Rp1,Rp2, …, Rpn}
。
(1)内部排序与外部排序
内部排序:整个排序过程不需要访问外存便能完成
外部排序:参加排序的记录数量很大,整个序列的排序过程不可能在内存中完成,需要借助外存
(2)主关键字与次关键字
上面所说的关键字 Ki可以是记录i的主关键字,也可以是次关键字,甚至可以是记录中若干数据项的组合。
若Ki是主关键字,则任何一个无序的记录序列经排序后得到的有序序列是唯一的。若Ki是次关键字或是记录中若干数据项的组合,得到的排序结果将是不唯一的,因为待排序记录的序列中存在两个或两个以上关键字相等的记录。
(3)排序的稳定性
若两个记录A和B的关键字值相等,若排序后A、B的先后次序保持不变,则称这种排序算法是稳定的,反之称为不稳定的。
(4)算法的优劣性
时间效率
:排序速度(排序所花费的全部比较次数)空间效率
:占内存辅助空间的大小稳定性
:排序是否稳定
注:本章均以升序排序为例。
8.1 插入排序
基本思想:在一个已排好序的记录子集的基础上,每一步将下一个待排序的记录有序插入到已排好序的记录子集中,直到将所有待排记录全部插入为止。
8.1.1 直接插入
算法思想:将第i
个记录插入到前面i-1
个已排好序的记录中。
具体过程:将第i
个记录的关键字Ki
,顺次与其前面记录的关键字Ki-1,Ki-2,…, K1
进行比较,将所有关键字大于*Ki的记录依次向后移动一个位置,直到遇见一个关键字小于或者等于*Ki
的记录Kj,此时Kj后面必为空位置,将第i
个记录插入空位置即可。
时间复杂度O(n2),空间复杂度为O(1),直接插入排序是一种稳定的排序方法。
若排序序列为{ 48,62, 35,77,55,14 ,35,98},下图给出用直接插入排序算法执行的过程(大括号内为当前已排好序的记录子集合):
void insert_sort(int a[],int n){ int i,j; int temp; for ( i=1; i<n; i++){ temp=a[i]; j=i-1; while ((j>=0)&& (temp<a[j])){ a[j+1]=a[j]; j--; } a[j+1]=temp; } }
8.1.2 折半插入
算法思想:在已形成的有序表中折半查找
,并在适当位置插入,把原来位置上的元素向后顺移。
折半查找相比与插入排序比较的次数大大减少,全部元素比较次数仅为 O(nlog2n) 。但其并未改变移动元素的时间耗费,所以时间效率仍然为为 O(n2) ,空间效率为 O(1) ,折半插入也是一种稳定的排序方法。
8.1.3 希尔排序
算法思想:先将整个待排记录序列分割成若干子序列, 分别进行直接插入排序, 待整个序列中的记录“基本有序”时,再对全体记录进行一次直接插入排序。
技巧:子序列的构成不是简单地“逐段分割”,而是将相隔某个增量dk的记录组成一个子序列。关于增量 d 的取法,最初提出取d=n/2,d=d/2
,直到d=1为止。该思路的缺点是,在奇数位置的元素在最后一步才会与偶数位置的元素进行比较,使得希尔排序效率降低。因此后来提出d=d/3+1
。
时间效率为 O(n1.25) ~ O(1.6n1.25) ,空间效率为 O(1) ,希尔插入是一种不稳定的排序方法。
若待排序序列为{ 46,55, 13,42,94,17 ,05,70},给出用希尔排序算法执行的过程:
//d是增量数组,numOfD是增量数组的大小。void shell_sort(int a[], int n , int d[] ,int numOfD) { int i,j,k,m; int val; int span;//增量 for(m=0; m<numOfD; m++) { span=d[m]; //span个小组 for(k=0; k<span; k++){ //组内进行直接插入排序 ,区别在于每次不是增加1,而是增加span for(i=k; i<n-span; i+=span) { val=a[j+span]; j=i; while(j>-1 && val<a[j]) { a[j+span]=a[j]; j=j-span; } a[j+span]=val; } } }}
8.2 交换排序
基于交换的排序法是一类通过交换逆序元素进行排序的方法。
8.2.1 冒泡排序
冒泡排序是一种简单的交换类排序方法,它是通过相邻的数据元素的交换,逐步将待排序序列变成有序序列的过程。
算法思想:每趟对所有记录从左到右相邻两个记录进行比较,若不符合排序要求,则进行交换。使用前提必需是顺序存储结构。
时间效率为 O(n2) ,空间效率为 O(1), 冒泡排序是一种稳定的排序方法。
若序列为{8, 3, 2, 5, 9, 3, 6},下图给出排序过程:
void bublle_sort(int a[],int n){ int i,j,temp; for(j=0;j<n-1;j++) for(i=0;i<n-1-j;i++) if(a[i]>a[i+1]){ temp=a[i]; a[i]=a[i+1]; a[i+1]=temp; }}
8.2.2 快速排序
改进要点:在冒泡排序中,由于扫描过程中只对相邻的两个元素进行比较,因此在互换两个相邻元素时只能消除一个逆序。如果能通过两个(不相邻的)元素的交换,消除待排序记录中的多个逆序,则会大大加快排序的速度。快速排序方法中的一次交换可能消除多个逆序。
算法思想:从待排序列中任取一个元素 (例如取第一个) 作为中心,所有比它小的元素一律前放,所有比它大的元素一律后放,形成左右两个子表;然后再对各子表重新选择中心元素并依此规则调整,直到每个子表的元素只剩一个。此时便为有序序列了。
快速排序的使用前提也是顺序存储结构,快速排序的最差时间复杂度和冒泡排序是一样的都是O(n2),它的平均时间复杂度为O(nlog2n),是一种不稳定的排序方法。
注:下面例子引用于【坐在马桶上看算法】算法3:最常用的排序——快速排序。
假设我们现在对“6 1 2 7 9 3 4 5 10 8”这个10个数进行排序。首先在这个序列中随便找一个数作为基准数。为了方便,以第一个数6作为基准数。
分别从序列两端开始“探测”。先从右往左找一个小于6的数,再从左往右找一个大于6的数,然后交换他们。这里可以用两个变量i和j,分别指向序列最左边和最右边。我们为这两个变量起个好听的名字“哨兵i”和“哨兵j”。刚开始的时候让哨兵i指向序列的最左边(即i=1),指向数字6。让哨兵j指向序列的最右边(即j=10),指向数字8。
首先哨兵j开始出动。因为此处设置的基准数是最左边的数,所以需要让哨兵j先出动,这一点非常重要。哨兵j一步一步地向左挪动(即j–),直到找到一个小于6的数停下来。接下来哨兵i再一步一步向右挪动(即i++),直到找到一个数大于6的数停下来。最后哨兵j停在了数字5面前,哨兵i停在了数字7面前。
现在交换哨兵i和哨兵j所指向的元素的值。交换之后的序列如下:6 1 2 5 9 3 4 7 10 8。
到此,第一次交换结束。接下来开始哨兵j继续向左挪动。他发现了4(比基准数6要小,满足要求)之后停了下来。哨兵i也继续向右挪动的,他发现了9(比基准数6要大,满足要求)之后停了下来。
此时再次进行交换,交换之后的序列如下:6 1 2 5 4 3 9 7 10 8。
第二次交换结束,“探测”继续。哨兵j继续向左挪动,他发现了3(比基准数6要小,满足要求)之后又停了下来。哨兵i继续向右移动,糟啦!此时哨兵i和哨兵j相遇了,哨兵i和哨兵j都走到3面前。说明此时“探测”结束。
我们将基准数6和3进行交换。交换之后的序列如下:3 1 2 5 4 6 9 7 10 8。
到此第一轮“探测”真正结束。此时以基准数6为分界点,6左边的数都小于等于6,6右边的数都大于等于6。
现在基准数6已经归位,它正好处在序列的第6位。此时我们已经将原来的序列,以6为分界点拆分成了两个序列,左边的序列是“3 1 2 5 4”,右边的序列是“9 7 10 8”,接下来还需要分别处理这两个序列。这是个递归的过程,直到将每个序列都排序完为止,下图展示了整个处理过程:
void quicksort(int a[], int left, int right) { int i, j, t, temp; if (left > right) return; temp = a[left]; //temp中存的就是基准数 i = left; j = right; while (i != j) { //顺序很重要,要先从右边开始找 while(a[j] >= temp && i<j) j--; //再找左边的 while (a[i] <= temp && i < j) i++; //交换两个数在数组中的位置 if (i < j) { t=a[i]; a[i]=a[j]; a[j]=t; } } //最终将基准数归位 a[left]=a[i]; a[i]=temp; quicksort(a, left,i-1);//继续处理左边的,这里是一个递归的过程 quicksort(a, i+1,right);//继续处理右边的 ,这里是一个递归的过程}
8.3 选择排序
在待排记录中依次选择关键字最小的记录作为有序序列的最后一条记录,逐渐缩小范围直至全部记录选择完毕。
8.3.1 简单选择排序
算法思想:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。
时间复杂度 О(n²),空间复杂度为О(1),简单选择排序是一种不稳定的排序方法。
void select_sort(int a[], int n){ int i, j, min, t; for( i =0; i < n -1; i ++) { min = i; for( j = i +1; j < n; j ++) if( a[min] > a[j]){ min = j; } if(min != i) { t = a[min]; a[min] = a[i]; a[i] = t; } } }
8.3.2 树型选择排序
算法思想:把带排序的n个记录的关键字两两进行比较,取出较小者。在[n/2]
个较小者中,采用同样的方法进行比较选出每两个中的较小者。如此反复,直至选出最小关键字记录为止。
树型选择排序又称为锦标赛法
,时间复杂度O(nlog2n),空间复杂度为O(n),树型选择排序是一种稳定的排序方法。相较于简单选择排序,虽然提升了时间复杂度,但是增加了空间复杂度,其本质是空间换时间。
若排序序列为{49, 38, 65, 97, 76, 13, 27, 49},根据算法思想,第一次选出最小关键字13:
将已经选出的13忽略掉(设置为∞),重新选出最小值,即27:
依此类推,就可以得到一个有序序列。
8.3.3 堆排序
算法思想:把待排序数组看成一颗完全二叉树,结点r[i]的左孩子是r[2i],右孩子是r[2i+1],双亲是r[i/2]。通过调整完全二叉树建堆及重建堆选择出关键字最小记录及次小记录等实现排序。
首先介绍下堆、小根堆和大根堆的概念:
堆(Heap):每个结点的值都大于、小于或等于其左右孩子结点的值
小根堆:每个结点的值都小于或等于其左右孩子结点的值
大根堆:每个结点的值都大于或等于其左右孩子结点的
时间复杂度:T(n)=O(nlog2n),空间复杂度:S(n)=O(1),堆排序是一种不稳定的排序方法。
以大根堆为例,首先我们根据堆定义建立初堆
。去掉最大元之后重建堆
,得到次大元。如此类推,完成堆排序。
重建堆步骤::
(1)将完全二叉树根结点中的关键字x移出,此时根结点相当于空结点。
(2)从空结点的左、右子中选出关键字最大的记录,如果该记录的关键字大于x,则将该记录上移至空结点。
(3)重复上述移动过程,直到空结点左、右子的关键字均不大于x。此时,将待调整记录放入空结点即可。
重建堆的调整方法相当于把待调整记录逐步向下“筛”的过程,所以一般称为筛选。
建初堆步骤:将一个任意序列看成是对应的完全二叉树,筛选需从最后一个子树位置[n/2]
开始,反复利用重建堆法自底向上,把所有子树逐层调整为堆,直至根节点。
堆排序步骤:
(1)建立初堆:从最后一子树n/2直到根建堆。
(2)将堆顶第一个元素与最后一个元素互换。
(3)去掉最后元素,将剩余元素调整建堆,再转出堆顶元素。
(4)重复执行(2)、(3)步骤n-1次,直到序列有序。
若关键字初始序列为:{48,62,35,77,55,14,35 ,98},给出堆排序的具体步骤:
/* 堆调整,构建大顶堆 arr[]是待调整的数组 i是待调整的数组元素的位置 length是数组的长度*/void HeapAdjust(int arr[], int i, int length) { int child, temp; for(; 2 * i + 1 < length; i = child) { //子节点的位置 = 2 * 父结点 + 1 child = 2 * i + 1; //得到子结点中较大的结点 if(child < length - 1 && arr[child + 1] > arr[child]) ++Child; //如果较大的子结点大于父结点那么把它往上移动替换它的父结点 if(arr[i] < arr[child]) { temp = arr[i]; arr[i] = arr[child]; arr[child] = temp; } else break; }}//堆排序算法 void HeapSort(int arr[], int length) { int i; /* 调整序列的前半部分元素。 调整完之后第一个元素是序列的最大元素 length/2-1是最后一个非叶子结点 */ for(i = length/2 - 1; i >= 0; --i) HeapAdjust(arr, i, length); /* 从最后一个元素开始对序列进行调整, 不断的缩小调整的范围直到第一个元素, 循环里是把第一个元素和当前的最后一个元素交换, 保证当前的最后一个位置的元素是现在这个序列的最大的, 不断的缩小调整heap的范围, 每一次调整完毕保证第一个元素是当前序列的最大的元素 */ for(i = length - 1; i > 0; --i) { arr[i] = arr[0]^arr[i]; arr[0] = arr[0]^arr[i]; arr[i] = arr[0]^arr[i]; //递归调整 HeapAdjust(arr, 0, i); }}
8.4 归并排序
前面介绍的插入排序、交换排序和选择排序,都是将一组记录按关键字大小排成一个有序的序列。而归并排序法
它的基本思想是基于合并,将两个或两个以上有序表合并成一个新的有序表。下面以二路归并
为例,介绍归并排序算法。
算法思想:假设初始序列含有 n 个记录,首先将这 n 个记录看成 n 个有序的子序列, 每个子序列的长度为 1,然后两两归并,得到[2/n]个长度为 2(n 为奇数时,最后一个序列的长度为 1)的有序子序列。在此基础上,再对长度为2的有序子序列进行两两归并,得到若干个长度为4的有 序子序列。如此重复,直至得到一个长度为 n的有序序列为止。
归并排序总的时间复杂度为 O(nlog2n) 。在实现归并排序时,需要和待排记录等数量的辅助空间,空间复杂度为 O(n) ,归并排序是一种稳定的排序方法。
归并排序根据其具体的实现,分为以下两种:
上往下排序
从下往上排序
/* * link from: http://www.cnblogs.com/skywang12345/p/3602369.html */#include <stdio.h>#include <stdlib.h>// 数组长度#define LENGTH(array) ((sizeof(array)) / (sizeof(array[0])))/* * 将一个数组中的两个相邻有序区间合并成一个 * * 参数说明: * a -- 包含两个有序区间的数组 * start -- 第1个有序区间的起始地址。 * mid -- 第1个有序区间的结束地址。也是第2个有序区间的起始地址。 * end -- 第2个有序区间的结束地址。 */void merge(int a[], int start, int mid, int end) { int *tmp = (int *)malloc((end-start+1)*sizeof(int)); //tmp是汇总2个有序区的临时区域 int i = start; // 第1个有序区的索引 int j = mid + 1; // 第2个有序区的索引 int k = 0; // 临时区域的索引 while(i <= mid && j <= end) { if (a[i] <= a[j]) tmp[k++] = a[i++]; else tmp[k++] = a[j++]; } while(i <= mid) tmp[k++] = a[i++]; while(j <= end) tmp[k++] = a[j++]; // 将排序后的元素,全部都整合到数组a中。 for (i = 0; i < k; i++) a[start + i] = tmp[i]; free(tmp);}/* * 归并排序(从上往下) * * 参数说明: * a -- 待排序的数组 * start -- 数组的起始地址 * endi -- 数组的结束地址 */void merge_sort_up2down(int a[], int start, int end) { if(a==NULL || start >= end) return ; int mid = (end + start)/2; merge_sort_up2down(a, start, mid); // 递归排序a[start...mid] merge_sort_up2down(a, mid+1, end); // 递归排序a[mid+1...end] // a[start...mid] 和 a[mid...end]是两个有序空间, // 将它们排序成一个有序空间a[start...end] merge(a, start, mid, end);}/* * 对数组a做若干次合并:数组a的总长度为len,将它分为若干个长度为gap的子数组; * 将"每2个相邻的子数组" 进行合并排序。 * * 参数说明: * a -- 待排序的数组 * len -- 数组的长度 * gap -- 子数组的长度 */void merge_groups(int a[], int len, int gap) { int i; int twolen = 2 * gap; // 两个相邻的子数组的长度 // 将"每2个相邻的子数组" 进行合并排序。 for(i = 0; i+2*gap-1 < len; i+=(2*gap)) { merge(a, i, i+gap-1, i+2*gap-1); } // 若 i+gap-1 < len-1,则剩余一个子数组没有配对。 // 将该子数组合并到已排序的数组中。 if ( i+gap-1 < len-1) { merge(a, i, i + gap - 1, len - 1); }}/* * 归并排序(从下往上) * * 参数说明: * a -- 待排序的数组 * len -- 数组的长度 */void merge_sort_down2up(int a[], int len) { int n; if (a==NULL || len<=0) return ; for(n = 1; n < len; n*=2) merge_groups(a, len, n);}void main() { int i; int a[] = {80,30,60,40,20,10,50,70}; int ilen = LENGTH(a); printf("before sort:"); for (i=0; i<ilen; i++) printf("%d ", a[i]); printf("\n"); merge_sort_up2down(a, 0, ilen-1); // 归并排序(从上往下) //merge_sort_down2up(a, ilen); // 归并排序(从下往上) printf("after sort:"); for (i=0; i<ilen; i++) printf("%d ", a[i]); printf("\n");}
一般情况下,由于要求附加和待排记录等数量的辅助空间,因此很少利用二路归并排序进行内部排序,归并的思想主要用于外部排序
。
外部排序分为以下两步:
(1)待排序记录分批读入内存,用某种方法在内存排序,组成有序的子文件,再按某种策略存入外存。
(2)子文件多路归并,成为较长有序子文件,再进入外存,如此反复,直到整个待排序文件有序。
8.5 总结
元素的移动次数与关键字的初始排列次序无关的是:基数排序。
元素的比较次数与初始序列无关是:选择排序。
算法的时间复杂度与初始序列无关的是:直接选择排序。
(1)简单排序法一般只用于 n 较小的情况(例如 n<30)。当序列中的记录“基本有序” 时,直接插入排序是最佳的排序方法。如果记录中的数据较多,则应采用移动次数较少 的简单选择排序法。
(2)快速排序、堆排序和归并排序的平均时间复杂度均为 O(nlogn),但实验结果表明,就平均时间性能而言,快速排序是所有排序方法中最好的。遗憾的是,快速排序在最坏情况下的时间性能为 O(n2)。堆排序和归并排序的最坏时间复杂度仍为 O(nlogn),当 n 较 大时,归并排序的时间性能优于堆排序,但它所需的辅助空间最多。
(3)可以将简单排序法与性能较好的排序方法结合使用。例如,在快速排序中,当划分 子区间的长度小于某值时,可以转而调用直接插入排序法;或者先将待排序序列划分成 若干子序列,分别进行直接插入排序,然后再利用归并排序法,将有序子序列合并成一 个完整的有序序列。
(4)基数排序的时间复杂度可以写成 O(d * n)。因此,它最适用于 n 值很大而关键字的位 数 d 较小的序列。当 d 远小于 n 时,其时间复杂度接近 O(n)。
(5)从排序的稳定性上来看,在所有简单排序法中,简单选择排序是不稳定的,其他各 种简单排序法都是稳定的。然而,在那些时间性能较好的排序方法中,希尔排序、快速 排序、堆排序都是不稳定的,只有归并排序、基数排序是稳定的。
综上所述,每一种排序方法各有特点,没有哪一种方法是绝对最优的。应根据具体情况选择合适的排序方法,也可以将多种方法结合起来使用。
8.6 例题
8.6.1 例1
希尔排序法、快速排序法、堆排序法和二路归并排序法四种排序法中,要求辅助空间最多的是【 】排序。
二路归并排序法
8.6.2 例2
快速排序、归并排序、堆排序、基数排序中,适合记录个数很大,但待排序关键字位数很少的排序算法是【 】。
基数排序
8.6.3 例3
设有5000个待排序的记录关键字,如果需要用最快的方法选出其中最小的10个记录关键字,则用下列【 】方法可以达到此目的。
A.归并排序 B.堆排序 C.基数排序 D.快速排序
B.堆排序
8.6.4 例4
在插入和选择排序中,若初始数据基本正序,则选用【 】;若初始数据基本反序,则选用【 】。在堆排序和快速排序中,若初始记录接近正序或反序,则选用【 】;若初始记录基本无序,则最好选用【 】。
插入排序 ; 选择排序 ; 堆排序 ; 快速排序
8.6.5 例5
设要将序列(Q, H, C, Y, P, A, M, S, R, D, F, X)中的关键码按字母序的升序重新排列,则:
冒泡排序一趟扫描的结果是【 】;
H C Q P A M S R D F X Y
初始步长为4的希尔(shell)排序一趟的结果是【 】;
P A C S Q D F X R H M Y
二路归并排序一趟扫描的结果是【 】;
H Q C Y A P M S D R F X
快速排序一趟扫描的结果是【 】;
F H C D P A M Q R S Y X
堆排序初始建堆的结果是【 】。
A D C R F Q M S Y P H X
- 数据结构 第八章 排序
- 《数据结构》 第八章 排序 笔记
- 数据结构复习——第八章:排序
- 数据结构笔记——第八章 排序技术
- JAVA数据结构和算法:第八章(排序)
- [数据结构]第八章-字典
- [数据结构]第八次作业:快速排序
- 第八章 排序
- 第八章 排序技术
- 第八章 排序技术
- 第八章 排序技术
- 第八章 排序技术
- 第八章 排序技术
- 第八章 排序技术
- 第八章 排序技术
- 第八章 排序
- 大话数据结构 -- 第八章 查找
- 数据结构(陈越)PAT练习题 第八周:排序(下)
- 128. Longest Consecutive Sequence
- java基础_设计模式_单例模式
- zookeeper for linux下载安装
- java面试题目01
- private 继承(effective C++ 条款40)
- 数据结构 第八章 排序
- 2017 ACM-ICPC 亚洲区(南宁赛区)网络赛
- 最近的计划
- 阿里云服务器配置(二)
- 深度优先搜索DFS
- java可重入锁(ReentrantLock)的实现原理
- Spring MVC--16.参数接受map
- 配置wordpress时出现403 Forbidden nginx/1.10.2
- python3 利用pip安装ipython notebook