From 57b36edf059355ae37c5961b486e921bd1fc2372 Mon Sep 17 00:00:00 2001 From: MYP Date: Sun, 29 Nov 2020 21:42:58 +0800 Subject: [PATCH] =?UTF-8?q?Create=2019=20=E6=8E=92=E5=BA=8F.md?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- DataStructureAndAlgorithm/19 排序.md | 679 +++++++++++++++++++++++++++ 1 file changed, 679 insertions(+) create mode 100644 DataStructureAndAlgorithm/19 排序.md diff --git a/DataStructureAndAlgorithm/19 排序.md b/DataStructureAndAlgorithm/19 排序.md new file mode 100644 index 0000000..b52a53b --- /dev/null +++ b/DataStructureAndAlgorithm/19 排序.md @@ -0,0 +1,679 @@ +# 19 排序 + +**知识结构:** + + +![图1 知识结构](https://img-blog.csdnimg.cn/20201129194657295.png) + + +## 1. 排序的基本概念与术语 + +假设含有$n$个记录的序列为$\lbrace r_1,r_2,\cdots,r_n \rbrace$,其相应的关键字分别为$\lbrace k_1,k_2,\cdots,k_n \rbrace$,需确定 的一种排列$1,2,\cdots,n$,使其相应的关键字满足$k_{p_1}\leq k_{p_2}\leq\cdots\leq k_{p_n}$的关系,即使得序列成为一个按关键字有序的序列 ,这个样的操作就称为 **排列**。 + + +能唯一标识某一记录的关键字称为 **主关键字**。 + +假设$k_i = k_j(1\leq i \leq n, 1\leq j\leq n,i\neq j)$,且在排序前的序列中$r_i$领先于$r_j$(即$i +/// 直接插入排序 +/// +/// 需要排序记录的类型 +/// 需要排序的记录集合 +public static void StraightInsertSort(T[] array) where T : IComparable +{ + for (int i = 1; i < array.Length; i++) + { + int j = i - 1; + T current = array[i]; + + while (j >= 0 && array[j].CompareTo(current) > 0) + { + array[j + 1] = array[j]; + j--; + } + array[j + 1] = current; + } +} +``` + +如果碰见一个和插入元素相等的,那么插入元素把想插入的元素放在相等元素的后面,所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以直接插入排序是稳定的。 + +### 2.2 希尔插入排序 + +希尔排序是1959年由D.L.Shell提出来的,相对直接插入排序有较大的改进。希尔插入排序又叫做**缩小增量排序**。 + +直接插入排序的效率在某些时候是很高的,比如我们的记录本身就是基本有序的,我们只需要少量的插入操作,就可以完成整个记录集的排序工作,此时直接插入排序很高效。还有就是记录数比较少时,直接插入的优势也比较明显。可问题在于,两个条件本身就过于苛刻,现实中记录少或者基本有序都属于特殊情况。 + +不过别着急,有条件当然是好,条件不存在,我们创造条件也是可以去做的。于是科学家希尔研究出了一种排序方法,对直接插入排序改进后可以增加效率。 + +思想:按待排序列下标的一定增量分组(如:增量序列$t_1,t_2,\cdots,t_k$,其中$t_i>t_j$,$t_k=1$),将整个待排序列分割成若干子序列,分别进行直接插入排序,随着增量逐渐减少,所分组包含的记录越来越多,到增量值减至1时,整个记录集被分成一组(这时待排序列“基本有序”),再对全体记录进行直接插入排序,算法终止(对待排序列进行了$k$趟直接插入排序)。 + +> 我所理解Shell Insertion Sort最牛的地方是,让排序算法能够并行化。 + +希尔插入排序增量的取法: + +$delta1=\left[\frac{n}{2}\right] =\left[\frac{10}{2}\right]=5$ + +$delta2=\left[\frac{delta1}{2}\right]=\left[\frac{5}{2}\right]=2$ + +$delta3=\left[\frac{delta2}{2}\right]=\left[\frac{2}{2}\right]=1$ + +即:先将要排序的一组记录按某个增量$delta$($\left[\frac{n}{2}\right]$ ,$n$为要排序数的个数)分成若干组子序列,每组中记录的下标相差$delta$。对每组中全部元素进行直接插入排序,然后在用一个较小的增量($\left[\frac{delta}{2}\right]$ )对它进行分组,在每组中再进行直接插入排序。继续不断缩小增量直至为1,最后使用直接插入排序完成排序。 + +如:`int[] Key = new int[]{45,34,78,12,34,32,29,64};` + +排序过程: + +$d=4\rightarrow 45,\overline{34},78,12,34,32,29,64$ + +$d=2\rightarrow 34,32,29,12,45,\overline{34},78,64$ + +$d=1\rightarrow 29,12,34,32,45,\overline{34},78,64$ + +$result\rightarrow 12,29,32,34,\overline{34},45,64,78$ + +程序代码: +```c +private static void Shell(int delta, T[] array) where T : IComparable +{ + //带增量的直接插入排序 + for (int i = delta; i < array.Length; i++) + { + int j = i - delta; + T current = array[i]; + while (j >= 0 && array[j].CompareTo(current) > 0) + { + array[j + delta] = array[j]; + j = j - delta; + } + array[j + delta] = current; + } +} + +/// +/// 希尔插入排序 +/// +/// 需要排序记录的类型 +/// 需要排序的记录集合 +public static void ShellInsertSort(T[] array) where T : IComparable +{ + for (int delta = array.Length/2; delta > 0; delta = delta/2) + { + Shell(delta, array); + } +} +``` + +希尔插入排序时效分析很难,关键码的比较次数与记录移动次数依赖于增量因子序列$delta$的选取。目前还没有人给出选取最好的增量因子序列的方法。希尔插入排序方法是一个不稳定的排序方法。 + +## 3. 选择排序 + +### 3.1 直接选择排序 + +思想:每一趟在$n-i(i=0,1,\cdots,n-1)$个记录中选取关键字最小的记录作为有序序列中第$i$个记录。(在要排列的一组数中,选出最小的一个数与第1个位置的数交换;然后在剩下的数当中再找最小的与第2个位置的数交换,依次类推,直到第$n-1$个元素和第$n$个元素比较为止。) + +如:`int[] Key = new int[]{45,34,78,12,34,32,29,64};` + +排序过程: + +$i=0\rightarrow 12,\overline{34},78,45,34,32,29,64$ + +$i=1\rightarrow 12,29,78,45,34,32,\overline{34},64$ + +$i=2\rightarrow 12,29,32,45,34,78,\overline{34},64$ + +$i=3\rightarrow 12,29,32,34,45,78,\overline{34},64$ + +$i=4\rightarrow 12,29,32,34,\overline{34},78,45,64$ + +$i=5\rightarrow 12,29,32,34,\overline{34},45,78,64$ + +$i=6\rightarrow 12,29,32,34,\overline{34},45,64,78$ + + +程序代码: +```c +/// +/// 直接选择排序 +/// +/// 需要排序记录的类型 +/// 需要排序的记录集合 +public static void StraightSelectSort(T[] array) where T : IComparable +{ + for (int i = 0; i < array.Length - 1; i++) + { + int minIndex = i; + T min = array[i]; + + for (int j = i + 1; j < array.Length; j++) + { + if (array[j].CompareTo(min) < 0) + { + min = array[j]; + minIndex = j; + } + } + if (minIndex != i) + { + array[minIndex] = array[i]; + array[i] = min; + } + } +} +``` + +直接选择排序是不稳定的。 + +### 3.2 堆选择排序 + +直接选择排序并没有把每一趟的比较结果保存下来,在后一趟的比较中,许多比较在前一趟已经做过了,但由于前一趟排序时未保存这些比较结果,所以后一趟排序时又重复执行了这些比较操作,因而执行的比较次数较多。 + +如果可以做到每次在选择到最小记录的同时,并根据比较结果对其他记录做出相应的调整,那样排序的总体效率就会非常高了。而堆选择排序是一种树形选择排序,是对直接选择排序的有效改进。堆选择排序算法是Floyd和Williams在1964年共同发明的,同时,他们也发明了“堆”这样的数据结构。 + +堆的概念:具有$n$个元素的序列$K=\lbrace k_1,k_2,⋯,k_n \rbrace$当且仅当满足 + + +$$ +\begin{cases} +k_i\leq k_{2i}\\ +k_i\leq k_{2i+1} +\end{cases}, +\begin{cases} +k_i\geq k_{2i}\\ +k_i\geq k_{2i+1} +\end{cases}, +i=1,2,\cdots,\left[\frac{n}{2}\right] +$$ + +时称之为堆。 + +若关键码的集合$K=\lbrace k_1,k_2,⋯,k_n \rbrace$,把它们按照完全二叉树的顺序存放在一维数组中。 +- 若满足$k_i\leq k_{2i}$且$k_i\leq k_{2i+1}$则称作小根堆。 +- 若满足$k_i\geq k_{2i}$且$k_i \geq k_{2i+1}$则称作大根堆。 + +小根堆:`int[] Key = new int[]{9,17,65,23,45,78,87,53,31,58,64};` + +![图3 小根堆](https://img-blog.csdnimg.cn/20201129161601262.png) + + + +大根堆:`int[] Key = new int[]{94,93,75,91,85,44,51,18,48,58,10,34};` + +![图4 大根堆](https://img-blog.csdnimg.cn/20201129161623478.png) + +思想(以大根堆为例): +构建大根堆之后,输出堆顶记录,对剩余的$n-1$个记录接着构建大根堆,便可得到$n$个记录的次大值,如此反复执行,就能得到一个有序序列,这个过程称为堆选择排序。 + +堆选择排序需解决的两个问题: + +问题1:如何建堆,对初始序列建堆的过程,就是一个反复进行筛选的过程。 + +- (1)对一棵具有$n$个结点的完全二叉树来说最后一个结点是第$[\frac{n}{2}]$个结点的子树。 +- (2)筛选从第$[\frac{n}{2}]$个结点为根的子树开始,该子树成为堆。 +- (3)之后向前依次对各结点为根的子树进行筛选,使之成为堆,直到根结点。 + +即把序号为$[\frac{n}{2}],[\frac{n}{2}]-1,\cdots,1$,的记录作为根的子树都调整为堆。 + +问题2:输出堆顶元素后,如何调整新堆。 + +- (1)设有$n$个元素的堆,输出堆顶元素后,剩下$n-1$个元素。将堆底元素送入堆顶(最后一个元素与堆顶进行交换),堆被破坏,其原因仅是根结点不满足堆的性质。 +- (2)将根结点与左,右子树中较大元素进行交换。 +- (3)若与左子树交换:如果左子树堆被破坏,即左子树的根结点不满足堆的性质,则重复第二步。 +- (4)若与右子树交换:如果右子树堆被破坏,即右子树的根结点不满足堆的性质,则重复第二步。 +- (5)继续对不满足堆性质的子树进行上述操作,直到叶子结点,堆被重建成。 + +即:输出堆顶元素,将最后一个叶子结点放在堆顶,重新构建大根堆。 + +如:`int[] Key = new int[]{45,34,78,12,34,32,29,64};` + +排序过程: + +初始时:$Key1 \rightarrow -1,45,\overline{34},78,12,34,32,29,64$ + +建堆后:$Key1 \rightarrow -1,78,64,45,\overline{34},34,32,29,12$ + +![图5 构建大根堆](https://img-blog.csdnimg.cn/20201129163644433.png) + + +堆重建后: + +$Key1 \rightarrow -1,64,\overline{34},45,12,34,32,29,[78]$ + +$Key1 \rightarrow -1,45,\overline{34},32,12,34,29,[64,78]$ + +$Key1 \rightarrow -1,\overline{34},34,32,12,29,[45,64,78]$ + +$Key1 \rightarrow -1,34,29,32,12,[\overline{34},45,64,78]$ + +$Key1 \rightarrow -1,32,29,12,[34,\overline{34},45,64,78]$ + +$Key1 \rightarrow -1,29,12,[32,34,\overline{34},45,64,78]$ + +$Key1 \rightarrow -1,12,[29,32,34,\overline{34},45,64,78]$ + +![图6 堆重建](https://img-blog.csdnimg.cn/20201129164627950.png) + +程序代码: +```c +private static void Restore(T[] array, int j, int vCount) where T : IComparable +{ + //构建以结点j为根,一共有vCount个结点的大根堆 + while (j <= vCount / 2) + { + int m = (2 * j + 1 <= vCount && array[2 * j + 1].CompareTo(array[2 * j]) > 0) + ? 2 * j + 1 + : 2 * j; + if (array[m].CompareTo(array[j]) > 0) + { + T temp = array[m]; + array[m] = array[j]; + array[j] = temp; + j = m; + } + else + { + break; + } + } +} + +/// +/// 堆选择排序 +/// +/// 需要排序记录的类型 +/// 需要排序的记录集合 +public static void HeapSelectSort(T[] array) where T : IComparable +{ + int vCount = array.Length; + T[] tempArray = new T[vCount + 1]; + + for (int i = 0; i < vCount; i++) + tempArray[i + 1] = array[i]; + + //初建大根堆 + for (int i = vCount / 2; i >= 1; i--) + Restore(tempArray, i, vCount); + + //大根堆的重构与排序 + for (int i = vCount; i > 1; i--) + { + T temp = tempArray[i]; + tempArray[i] = tempArray[1]; + tempArray[1] = temp; + Restore(tempArray, 1, i - 1); + } + + for (int i = 0; i < vCount; i++) + array[i] = tempArray[i + 1]; +} +``` + +## 4. 交换排序 + +### 4.1 冒泡交换排序 + +思想:通过相邻记录之间的比较和交换,使关键字较小的记录如气泡一般逐渐向上漂移直至水面。 + +如:`int[] Key = new int[]{45,34,78,12,34,32,29,64};` + +$i=0\rightarrow [12],45,\overline{34},78,29,34,32,64$ + +$i=1\rightarrow [12,29],45,\overline{34},78,32,34,64$ + +$i=2\rightarrow [12,29,32],45,\overline{34},78,34,64$ + +$i=3\rightarrow [12,29,32,\overline{34}],45,34,78,64$ + +$i=4\rightarrow [12,29,32,\overline{34},34],45,64,78$ + +$i=5\rightarrow [12,29,32,\overline{34},34,45],64,78$ + +$i=6\rightarrow [12,29,32,\overline{34},34,45,64],78$ + + +程序代码: +```c +/// +/// 冒泡交换排序 +/// +/// 需要排序记录的类型 +/// 需要排序的记录集合 +public static void BubbleExchangeSort(T[] array) where T : IComparable +{ + for (int i = 0; i < array.Length - 1; i++) + { + for (int j = array.Length - 1; j > i; j--) + { + if (array[j].CompareTo(array[j - 1]) < 0) + { + T temp = array[j - 1]; + array[j - 1] = array[j]; + array[j] = temp; + } + } + } +} +``` + +对冒泡排序常见的改进方法是加入一个标志性变量`flag`,用于标志某一趟排序过程是否有数据交换,如果进行某一趟排序时并没有进行数据交换,则说明数据已经按要求排列好,可立即结束排序,避免不必要的比较过程。 + + +程序代码: +```c +/// +/// 改进的冒泡交换排序 +/// +/// 需要排序记录的类型 +/// 需要排序的记录集合 +public static void BubbleExchangeSortImproved(T[] array) where T : IComparable +{ + for (int i = 0; i < array.Length - 1; i++) + { + bool flag = false; + for (int j = array.Length - 1; j > i; j--) + { + if (array[j].CompareTo(array[j - 1]) < 0) + { + T temp = array[j - 1]; + array[j - 1] = array[j]; + array[j] = temp; + flag = true; + } + } + if (flag == false) + break; + } +} +``` + +### 4.2 快速交换排序 + +快速交换排序是由图灵奖获得者Tony Hoare(东尼.霍尔)所发展的一种排序算法,是采用分治策略的一个非常典型的应用。快速交换排序虽然高端,但其思想是来自冒泡交换排序的,冒泡交换排序是通过相邻元素的比较和交换把最小的冒泡到最顶端,而快速交换排序是比较和交换小数和大数,这样一来不仅小数冒泡到上面的同时也把大数沉到下面。 + +其基本思想如下: + +- (1)选择一个基准元素,通常选择第一个元素或者最后一个元素。 +- (2)通过一趟排序将待排序的记录分割成独立的两部分,其中一部分记录的元素值均比基准元素值小,另一部分记录的元素值比基准值大。 +- (3)此时基准元素在其排好序的正确位置。 +- (4)然后分别对这两部分记录用同样的方法继续进行排序,直到整个序列有序。 + +即:从待排记录中选一记录,将其放入正确的位置,然后以该位置为界,对左右两部分再做快速排序,直到划分的长度为1。 + +如:`int[] Key = new int[]{45,34,78,12,34,32,29,64};` + + +$init\rightarrow 45,\overline{34},78,12,34,32,29,64$ + +$step1\rightarrow [32,\overline{34},29,12,34],45,[78,64]$ + +$step2\rightarrow [[29,12],32,[\overline{34},34]],45,[78,64]$ + +$step3\rightarrow [[[12],29],32,[\overline{34},34]],45,[78,64]$ + +$step4\rightarrow [[[12],29],32,[[34],\overline{34}]],45,[78,64]$ + +$step5\rightarrow [[[12],29],32,[[34],\overline{34}]],45,[[64],78]$ + + +程序代码: +```c +private static void QuickSort(T[] array, int left, int right) where T : IComparable +{ + //快速排序递归函数 + if (left < right) + { + T current = array[left]; + int i = left; + int j = right; + while (i < j) + { + while (array[j].CompareTo(current) > 0 && i < j) + j--; + while (array[i].CompareTo(current) <= 0 && i < j) + i++; + if (i < j) + { + T temp = array[i]; + array[i] = array[j]; + array[j] = temp; + j--; + i++; + } + } + array[left] = array[j]; + array[j] = current; + if (left < j - 1) QuickSort(array, left, j - 1); + if (right > j + 1) QuickSort(array, j + 1, right); + } +} + +/// +/// 快速交换排序 +/// +/// 需要排序记录的类型 +/// 需要排序的记录集合 +public static void QuickExchangeSort(T[] array) where T : IComparable +{ + QuickSort(array, 0, array.Length - 1); +} +``` + +其实上面的代码还可以再优化,上面代码中基准元素已经在`current`中保存了,所以不需要每次交换都设置一个`temp`变量,在交换的时候只需要先后覆盖就可以了。这样既能较少空间的使用还能降低赋值运算的次数。 + +优化代码如下: + +```c +private static void QuickSortImproved(T[] array, int left, int right) where T : IComparable +{ + //快速排序递归函数 + if (left < right) + { + T current = array[left]; + int i = left; + int j = right; + while (i < j) + { + while (array[j].CompareTo(current) > 0 && i < j) + j--; + array[i] = array[j]; + + while (array[i].CompareTo(current) <= 0 && i < j) + i++; + array[j] = array[i]; + } + array[j] = current; + if (left < j - 1) QuickSortImproved(array, left, j - 1); + if (right > j + 1) QuickSortImproved(array, j + 1, right); + } +} + +/// +/// 快速交换排序 +/// +/// 需要排序记录的类型 +/// 需要排序的记录集合 +public static void QuickExchangeSortImproved(T[] array) where T : IComparable +{ + QuickSortImproved(array, 0, array.Length - 1); +} +``` + +## 5. 并归排序 + +我们首先先看两个有序序列合并的例子,如: + +```c +int[] Key1 = new int[]{1,3,5,7,9}; +int[] Key2 = new int[]{2,4,6,8,10,12,14} + +int[] temp = new int[Key1.Length + Key2.Length]; +``` +$temp \rightarrow 0,0,0,0,0,0,0,0,0,0,0,0$ + +$temp \rightarrow 1,2,3,4,5,6,7,8,9,0,0,0$ + +$temp \rightarrow 1,2,3,4,5,6,7,8,9,10,12,14$ + +程序代码: + +```c +/// +/// 合并排序 +/// +/// 需要排序记录的类型 +/// 有序记录集合1 +/// 有序记录集合2 +/// 合并后的有序记录集合 +public static T[] MergeSort(T[] array1,T[] array2) where T : IComparable +{ + T[] temp = new T[array1.Length + array2.Length]; + int i = 0, j = 0, k = 0; + while (i < array1.Length && j < array2.Length) + { + if (array1[i].CompareTo(array2[i]) < 0) + { + temp[k++] = array1[i++]; + } + else + { + temp[k++] = array2[j++]; + } + } + while (i < array1.Length) + { + temp[k++] = array1[i++]; + } + while (j < array2.Length) + { + temp[k++] = array2[j++]; + } + return temp; +} +``` + +我们接着看一个序列的并归排序。 + +首先递归划分子问题,然后合并结果。把待排序列看成由两个有序的子序列构成,然后合并两个子序列,接着把子序列看成由两个有序序列组成……。倒过来看,其实就是先两两合并,然后四四合并……最终形成有序序列。该算法是采用分治策略的一个非常典型的应用,俗称 **2-路并归**。 + +如:`int[] Key = new int[]{45,34,78,12,34,32,29,64};` + +![图7 并归排序](https://img-blog.csdnimg.cn/20201129192544101.png) + +程序代码: +```c +/// +/// 合并排序的递归合并函数 +/// +/// 需要排序记录的类型 +/// 需要排序的记录集合 +/// 起点位置 +/// 中间位置 +/// 终点位置 +private static void Merge(T[] array, int left, int mid, int right) where T : IComparable +{ + T[] temp = new T[right - left + 1]; + int i = left; + int j = mid + 1; + int k = 0; + while (i <= mid && j <= right) + { + if (array[i].CompareTo(array[j]) < 0) + { + temp[k++] = array[i++]; + } + else + { + temp[k++] = array[j++]; + } + } + while (i <= mid) + { + temp[k++] = array[i++]; + } + while (j <= right) + { + temp[k++] = array[j++]; + } + for (int n = 0; n < temp.Length; n++) + { + array[left + n] = temp[n]; + } +} + +/// +/// 合并排序的递归分治函数 +/// +/// 需要排序记录的类型 +/// 需要排序的记录集合 +/// 起点位置 +/// 终点位置 +private static void MergeSort(T[] array, int left, int right) where T : IComparable +{ + if (left >= right) + return; + + int mid = (left + right) / 2; + MergeSort(array, left, mid); //递归排序左边 + MergeSort(array, mid + 1, right); //递归排序右边 + Merge(array, left, mid, right); //合并 +} + + +/// +/// 合并排序 +/// +/// 需要排序记录的类型 +/// 需要排序的记录集合 +public static void MergeSort(T[] array) where T : IComparable +{ + MergeSort(array, 0, array.Length - 1); +} +``` + +