当前位置: 首页 > news >正文

网站图怎么做基于jsp的网站开发

网站图怎么做,基于jsp的网站开发,ui培训内容,0基础学习网站建设文章目录 1. 预备知识2. 插入排序2.1 直接插入排序2.2 折半插入排序 3. 希尔排序4. 交换排序4.1 冒泡排序4.2 快速排序4.2.1 选取基准值4.2.2 分割策略4.2.3 小数组4.2.4 基于Hoare版本 最后优化 递归版本 快速排序4.2.5 快速排序的非递归版本4.2.6 快速排序的分析 5. 选择排序… 文章目录 1. 预备知识2. 插入排序2.1 直接插入排序2.2 折半插入排序 3. 希尔排序4. 交换排序4.1 冒泡排序4.2 快速排序4.2.1 选取基准值4.2.2 分割策略4.2.3 小数组4.2.4 基于Hoare版本 最后优化 递归版本 快速排序4.2.5 快速排序的非递归版本4.2.6 快速排序的分析 5. 选择排序5. 1 简单选择排序5. 2 堆排序 6. 归并排序7. 计数排序8. 基数排序9. 排序算法复杂度及稳定性总结 在这一章, 我们讨论数组元素的排序问题. 为简单起见, 假设在我们的例子中数组只包含整数, 如果是结构体等复杂的数据则需要考虑到排序稳定性等, 在文章最后会进行讨论. 同样, 所有的排序将能够在主存中完成, 即元素的个数相对来说比较小(小于 1 0 6 10^6 106).当然, 不能在主存中完成的而必须在磁盘上或磁带上完成的排序也相当重要. 这种类型的排序叫做外部排序(externeal sorting), 也会在文章末尾讨论, 主要是归并排序. 1. 预备知识 排序: 所谓排序, 就是使一串记录, 按照其中的某个或某些关键字的大小, 递增或递减的排列起来的操作. (本章所有的排序将按递增排序) 稳定性: 假定在待排序的记录序列中, 存在多个具有相同关键字的记录, 若经过排序, 这些记录的相对次序保持不变, 即在原序列中, r[i]r[j], 且r[i]在r[j]之前, 而在排序后的序列中, r[i]仍在r[j]之前.则称这种排序算法是稳定的; 否则则称为不稳定的. 内部排序: 数据元素全部放在内存中的排序. (本章所有的排序都是内部排序, 归并排序同样也是外部排序) 外部排序: 数据元素太多不能同时放在内存中, 根据排序过程的要求不能在内外存之间移动数据的排序 基于比较的排序: 根据符号 而进行排序的排序算法, 称作基于比较的排序, 而这种排序均需要 Ω ( N l o g N ) \Omega(N logN) Ω(NlogN)次比较. 对于排序代码的书写, 推荐先把单趟写出来, 再考虑整体一共要进行多少趟. 最初我们学习冒泡排序也是这个思想. 2. 插入排序 每次将一个待排序的记录按其关键字大小插入前面已经排好序的子序列, 直到全部记录插入完成. 插入排序的思想可以引申出三个重要的排序算法: 直接插入排序, 折半插入排序和希尔排序. 2.1 直接插入排序 假设在排序过程中, 待排序表 L[0…N-1] 在某次排序过程中某一时刻状态如下: 要将下标为 i 的元素插入到有序序列中需要进行如下操作: 将L[i] 从后向前依次与有序序列的元素进行比较;如果L[i] 比 L[k] 要小, 将 L[k] 向后移动一个元素位置, k k-1. 重复步骤2如果L[i] 大于等于 L[k], 将 L[k] 向后移动一个元素位置, L[i] 插入到 L[k] 位置上如果 k 0, 则直接结束, 将 L[i] 插入到 L[0] 的位置上 第一轮将L[0]视为有序序列, 即从L[1]开始到L[N-1]进行每一轮的插入, 一共需要进行 N-1 轮, 能将表 L[0…N-1]排序成一个有序序列. 真正实施交换的时机只有在找到L[i] 应该插入位置的时候. 代码实现 void InsertSort(int* a, int n) {int i 0;for (i 0; i N-1; i){int end i; //end 记录有序序列最后一个元素下标int key a[end 1]; //key 记录无序序列第一个元素下标while (end 0){if (key a[end]){a[end1] a[end];end--;}else {break;}}//插入数据a[end1] key;} }将有序数组的最后一个元素的下标定义为end每次将end1 的元素插入到有序数组中,从end一直比较到0 如果比有序数组中的元素小 则将有序数组中的元素向后移动一位接着向前比较 如果比有序数组中的元素大 则直接break 跳出循环后将元素插入到end1位置 一共进行 n1轮 end从0到n-2 直接插入排序算法的特性总结: 空间复杂度: 仅使用了常数个辅助单元, 空间复杂度为 O ( N ) O(N) O(N) 时间复杂度: 嵌套循环每趟花费 N 次迭代, 因此直接插入排序为 O ( N 2 ) O(N^2) O(N2), 而且这个界是精确的 最好情况下: 表中元素已经有序, 此时每插入一个元素, 都只需比较一次而不用移动元素, 一共比较 N-1 次. 因而最好情况下时间复杂度为 O ( N ) O(N) O(N)最坏情况下: 表中元素全为逆序, 此时一共需要移动 ∑ i 0 n − 2 i 1 1 2 . . . ( n − 1 ) Θ ( N 2 ) \sum_{i0}^{n-2} {i1} 1 2 ... (n-1) \Theta(N^2) ∑i0n−2​i112...(n−1)Θ(N2), 因而最坏情况下时间复杂度为 O ( N 2 ) O(N^2) O(N2) 稳定性: 由于每次插入元素总是从后向前比较再移动, 所以不会出现相同元素相对位置变化的情况. 直接插入排序是稳定的排序方法. 2.2 折半插入排序 从直接插入排序算法中, 不难看出每趟插入的过程中都进行了两项工作 从前面的有序序列中查找到待插入元素应该插入的位置给插入位置腾出空间, 再将元素插入进去. 在直接插入排序中, 边比较边移动. 其实可以直接将查找和移动这两项工作分开. 先折半查找出元素应该插入的位置, 再给插入的元素腾出位置 代码实现如下: void InsertSort(int* a, int n) {int i, j, left, right, mid;for (i 0; i N-1; i){int end i; //end 记录有序序列最后一个元素下标int key a[end 1]; //key 记录无序序列第一个元素下标 left 0;right end;while (left right){//最后找到的right1即为元素应该插入的位置mid left / 2 right / 2;if (a[mid] key){right mid - 1; //查找左半部分有序序列}else{left mid 1; //查找右半部分有序序列}}for (j end; j right1; j--){a[j1] a[j];}a[right1] key;} }虽然上述算法将查找的时间复杂度缩减为 O ( N l o g N ) O(NlogN) O(NlogN), 但是移动元素的时间复杂度仍然是 O ( N 2 ) O(N^2) O(N2).所以折半排序的时间复杂度仍为 O ( N 2 ) O(N^2) O(N2). 但是对于数据量不是很大的排序表, 折半插入排序往往能够表现出很好的性能. 折半插入排序也是一种稳定的排序算法. 3. 希尔排序 从前面的分析可以得知, 直接插入排序在数组有序的情况下时间复杂度为 O ( N ) O(N) O(N), 也就是说, 直接插入排序适用于基本有序和数据量不大的有序表. 希尔排序正是由这两点分析对直接插入排序进行改进得来的, 又称 缩小增量排序(diminishing increment sort) 希尔序列使用一个序列: h 1 , h 2 , h 3 , . . . , h t h_1, h_2, h_3,...,h_t h1​,h2​,h3​,...,ht​, 叫做增量序列(increment sequence). 只要 h 1 1 h_1 1 h1​1, 任何增量序列都是可行的,不过有些增量序列更好. 从 h t 到 h 1 h_t到h_1 ht​到h1​, 在使用增量为 h k h_k hk​的排序之后, 对于每一个 i, 都有 A [ i ] A [ i h k ] A[i] A[ih_k] A[i]A[ihk​], 即所有相隔 h k h_k hk​的元素都是被排序的, 称为 h k − {h_k}^- hk​−排序的. 注意每次取得间隔是从 h t 到 h 1 h_t到h_1 ht​到h1​的(即依次减小的), 当间隔为 h 1 1 h_1 1 h1​1 时, 该序列有序. Shell建议的序列: h t ⌊ N / 2 ⌋ 和 h k ⌊ h k 1 / 2 ⌋ h_t \lfloor N/2 \rfloor 和 h_k \lfloor h_{k1} / 2 \rfloor ht​⌊N/2⌋和hk​⌊hk1​/2⌋. (流行但是不好). 总的来说思路就是: 使用一个 h 1 1 h_11 h1​1的增量序列, 每次 h k − {h_k}^- hk​−排序使用直接插入排序. 希尔排序的意义: 小的数据迅速移动到序列前部分, 大的数据迅速移动到序列后部分, 同时使用直接插入排序对于有序序列排序接近 O ( N ) O(N) O(N)的优点. 希尔增量的代码实现: void ShellSort(int* a, int n) {int gap n;while (gap 1){gap / 2;int i 0;for (i 0; i n-gap; i){ //进行间隔为gap的排序int end i; //记录有序序列的最后一个元素int tmp a[endgap]; //记录无序序列的第一个元素while (end 0){if (tmp a[end]){a[endgap] a[end];end - gap;}else {break;}}//插入元素a[endgap] tmp;}} }希尔排序的最坏情况分析 希尔排序的运行时间依赖于增量序列的选择 而证明可能相当复杂 下面将证明两个特别的增量序列下最坏情况的精确的界 使用希尔增量时希尔排序的最坏情形运行时间为 Θ ( N 2 ) \Theta(N^2) Θ(N2) 举个例子{1,9,2,10,3,11,4,12,5,13,6,14,7,15,8,16} 这一串序列有N 16 个元素也就是说使用希尔增量则每次的间隔都为偶数(除了最后一个1)它的偶数位有N/2个同为最大的数奇数位上有N/2个同为最小的数 这样在最后一次间隔为 1 的排序前所有的数仍然保持原来的位置不变。 最后一趟排序需要将第 2i-1 个数移动到第 i 个位置仅仅将最小的数放到正确位置就需要 ∑ i 1 N / 2 i − 1 Ω ( N 2 ) \sum_{i1}^{N/2}{i-1} \Omega(N^2) ∑i1N/2​i−1Ω(N2)的工作量。 所以希尔排序最坏情况是可以 Θ ( N 2 ) \Theta(N^2) Θ(N2)的原因就是增量序列有公因子对一些特殊情况可能效率极低。 Hibbard提出了稍微不同的增量序列形如 1 , 3 , 7 , … , 2 k − 1 1,3,7,…,2^{k-1} 1,3,7,…,2k−1这个序列的最坏运行时间为 Θ ( N 3 / 2 ) \Theta(N^{3/2}) Θ(N3/2) Sedgewick提出的增量序列 9 ∗ 4 i − 9 ∗ 2 i 1 9*4^i-9*2^i1 9∗4i−9∗2i1可以达到 O ( N 4 / 3 ) O(N^{4/3}) O(N4/3) 总之 希尔排序的时间复杂度可以认为是 O ( N 1.3 ) O(N^{1.3}) O(N1.3) 但是希尔排序不是一个稳定的排序算法如果相同的数被分到了不同的组后一个数是有可能被移动到前面去的。 4. 交换排序 根据序列中的两个元素关键字的比较结果来对换这两个记录在序列中的位置 本文主要涉及冒泡排序和快速排序 4.1 冒泡排序 由后往前由前往后两两比较相邻的值若两值为逆序即(a[i-1]a[i]), 则交换两值直到序列比较完。步骤一得到的最小最大元素不再参与排序继续新的一轮的步骤一每轮不断得到当前序列的最小最大值并将最小最大值放置序列的头尾最多需要 n-1轮 代码实现 void BubbleSort(int* a, int n) {int i 0;for (i 0; i n-1; i){int exchange 0;int j 0;for (j 0; j n-1-i; j){if (a[j-1] a[j]){Swap(a[j-1], a[j]);exchange 1;}}if (exchange 0){break;}} }冒泡排序算法的分析 空间复杂度仅使用了常数个辅助单元空间复杂度为 O ( 1 ) O(1) O(1)时间复杂度冒泡排序分为比较和交换。 最好情况当序列本身有序时 需要比较 n-1次 此时未发生交换直接跳出循环时间复杂度为 O ( N ) O(N) O(N)最坏情况当序列全部逆序的时候则一共需要比较 ∑ i 1 n − 1 n − i n ( n − 1 ) 2 \sum_{i1}^{n-1}{n-i} \frac{n(n-1)}{2} ∑i1n−1​n−i2n(n−1)​而移动次数需要 ∑ i 1 n − 1 3 ( n − i ) 3 n ( n − 1 ) 2 \sum_{i1}^{n-1}{3(n-i)} \frac{3n(n-1)}{2} ∑i1n−1​3(n−i)23n(n−1)​ 由此可以看出冒泡排序的时间复杂度为 O ( N 2 ) O(N^ 2) O(N2) 稳定性由于相邻两个元素只有前一个元素大于后面一个元素才会发生交换所以当i j时且a[i] a[j]时两个元素的顺序不会发生改变 注意冒泡排序后的所产生的有序序列一定是全局有序的即有序序列中所有的关键字一定大于小于剩余无序序列的所有关键字 4.2 快速排序 快速排序是Hoare提出的一种基于二叉树结构的排序算法。其思路主要是首先选取序列中某一个关键字当作基准值 随后将整个序列分成左右序列左序列的所有关键字小于基准值右序列的所有关键字大于等于基准值。随后左右序列重复该过程直至整个序列的元素全部有序 快速排序是一种分治的递归算法。将数组 S 排序的基本算法分为下面四步 1 如果 S 中元素个数是 1 或者 0 直接返回 2 取 S 中任一元素 v 称为该元素为基准值也叫枢纽元pivot 3 将 S - {v}S中其余元素分成两个不相关的集合其中一个集合 S 1 S_1 S1​所有元素小于基准值另一个集合 S 2 S_2 S2​的所有元素大于等于基准值 4 最后返回QuickSort(S1)后继而 v 继而返回QuickSort(S2) 对于基准值的取法步骤2和如何分割步骤3这是不唯一的。可以预想到的如果每次的基准值都是序列的中位数这样递归形成的二叉树可以看作一个平衡二叉树由于递归所消耗的时间是线性时间的这样的情况能保证递归次数最少这样整个算法的时间复杂度也会低。 4.2.1 选取基准值 虽然上面的步骤无论选择哪个元素当作基准值都是可以完成排序的但是经过分析肯定是每次都能选到序列的中位数当作基准值是最好的 一种错误的方法 一种方法是选择序列的第一个元素当作基准值。如果输入的序列是随机的那么结果还可以接受但是如果输入的序列是预排序的或者是反序的那么以这种策略造成的分割是恶劣的所有的元素不是被分到 S 1 就是 S 2 S_1就是S_2 S1​就是S2​中更有可能的是接下来的所有分割都是这种情况的。那么一共会有 N-1 次分割而每次分割的时间复杂度是 O ( N ) O(N) O(N)的这样整个算法的时间复杂度会直接到达 O ( N 2 ) O(N^2) O(N2)这显然不是我们想得到的。 一种安全的方法 另一种方法是使用随机数随机选取基准值。一般来说这样不会每次都是恶劣的分割但是生成随机数消耗并不小也减小不了多少时间 三数中值分割法 Median-of-Three Partitioning 一般的方法是使用左端右端和中心位置的中值作为基准值。显然得到整个序列的中值消耗是十分巨大的这样的策略能几乎避免第一种每次都是恶劣分割的情形。例如有一串序列{8,1,3,7,6,2,8,0,4},它的左端值是8右端值是4中心位置4的值是6于是基准值为6.这样能减小快速排序接近5%的运行时间。 int getMidi(int* a, int left, int right) {int mid left/2 right/2;if (a[mid] a[left]){if (a[left] a[right]) //此时mid最大{return left;}else if (a[mid] a[right]){return right;}else{return mid;}}else //a[mid] a[left]{if (a[right] a[left]) //此时mid最小{return left;}else if (a[mid] a[right]){return right;}else {return mid;}} }4.2.2 分割策略 Hoare版本 左右指针分别指向序列左端和右端。 右指针先向左移动直到遇到比基准值小的值左指针随后向右移动直到遇到比基准值大的值 将左右指针指向的两个元素进行交换一次分割结束的标志是左右指针相遇最后将基准值的和相遇位置的元素进行交换 代码实现 int Partition(int* a, int left, int right) {int mid getMidi(a, left, right);Swap(a[left], a[mid]);int keyi left;while (left right){while(left right a[keyi] a[right]){//先找右小right--;}while (left right a[keyi] a[left]){//再找左大left}//交换两值Swap(a[left], [right])}Swap(a[left], a[keyi]);return left; }void QuickSort(int* a, int left, int right) {if (right left)return;int keyi Partition(a, left, right);QuickSort(a, left, keyi-1);QuickSort(a, keyi1, right); }这个版本有三个点要注意 1找左和找右只能找大于和小于基准值的值而不是大于等于和小于等于这样是会造成死循环的 2在找右小左大的时候不能让 left 或者 right 越界 如果一直没有找到右小或者左大, 当left 和 right相遇的的时候需要跳出循环 所以需要在内部的while循环中也进行left right的判断 3交换的时候, 将下标为 keyi 和 相遇位置上的元素进行交换 因为实现已经将基准值交换到序列首元素位置,所以交换的是该段序列首元素和left 与right 相遇位置的元素;而不是直接交换 key 的值, key只是函数临时构建的局部变量, 并不会对原序列有影响. 所以最终记录了该序列首元素的下标 keyi. 这里还有一个问题: left 和 right 相遇的位置的元素一定比基准值小吗? 对于相遇的位置有两种情况: 右小找到了, 但是左大一直在寻找中, 最后相遇点就是 right 所在的位置, 这个位置上的元素一定比基准值小上一轮交换后, 此时 left 的位置上一定是比基准值小的值, 下一轮寻找, 右小没有找到, right 直接减小到该位置上, 所以这个情况下也是符合的 挖坑法 Hoare版本有很多的注意点, 有人就更新了一种新的策略, 这样不需要再额外考虑left 和 right 相遇点是否比基准值小 首先将基准值设为坑两个左右指针分别指向序列的左端和右端先找右小, 找到后将该位置元素放置坑位, 同时 right 此时设置为坑再找左大, 找到后将该位置元素放置坑位, 同时 left 此时设置为坑如果left 和 right 没有相遇, 重复步骤3, 4;如果相遇,将基准值放置坑位 代码实现: void Partition(int* a, int left, int right) {int mid geiMidi(a, left, right);Swap(a[left], a[mid]);int key a[left];int hole left;while (left right){while (left right a[right] key){right--;}//找到直接赋值到坑位, 同时将此时的right位置当作坑a[hole] a[right];hole right;while (left right a[left] key){left;}//找到直接赋值到坑位, 同时将此时的left位置当作坑a[hole] a[left];hole left;}a[hole] key;return left; }前后指针法 思路: 将基准值放置数组首元素前后指针 prev 指向序列开头, cur 指向prev下一个位置随后判断 cur 指向位置所在元素与基准值的大小 如果 cur 指向元素比基准值小, 则prev1, 同时将cur所指元素和prev所指元素交换, cur随后如果 cur 指向元素不比基准值小, prev 不动, 只有cur 一直重复步骤3直到 cur 越界, 此时交换 prev 指向位置元素和序列首元素进行交换 代码实现: int Partition(int* a, int left, int right) {int mid getMidi(a, left, right);Swap(a[left], a[mid]);int prev left;int cur prev 1;int keyi left;while (cur right){if (a[cur] a[keyi] prev ! cur){Swap(a[prev], a[cur]);} cur;}Swap(a[prev], a[keyi]);return prev; }prev指向的位置永远是比基准值小的序列的最后一个元素, 这样最后结束可以保证prev 指向位置上面的元素是比基准值小的. 4.2.3 小数组 我们知道, 满二叉树的最后一层的结点占据所有结点的一半. 我们假设快速排序是最好情况, 这样形成的递归树接近一个满二叉树, 而每个结点代表要开辟一块栈帧空间, 需要消耗线性时间 换一个方式来说, 如果需要排序的数组只有十个元素, 那么对此需要三层的二叉树, 消耗可能还不如插入排序(插入排序在数量小和接近有序的情况下花费 O ( N ) O(N) O(N)). 在数量比较大的数组进行快速排序中, 这三层恰好是整个递归二叉树的最下面三层左右, 如果能将这三层优化掉, 可以减少递归树约87.5%的结点个数. 措施是当序列结点小于 10 的时候, 直接使用插入排序, 能很好的发挥好插入排序在序列已经预排序且数量小的情况下时间复杂度为 O ( N ) O(N) O(N)这个优势 4.2.4 基于Hoare版本 最后优化 递归版本 快速排序 void QuickSort(int* a, int left, int right) {// 每一次排序可以确认一个数的位置, 再在这个数左右进行二分继续排序if (left right)return ;int mid getMidi(a, left, right);Swap(a[left], a[mid]);int keyi left;// 小区间情况 right-left1 10, 直接插入排序可以省去87.5%的递归子树int size right - left 1;if (size 10){InsertSort(a, size);}else {while (left right){// 先从right开始向前找比key小的值while (left right a[right] a[keyi]){right--;}// 再从left开始向后找比key大的值while (left right a[left] a[keyi]){left;}Swap(a[left], a[right]);}}Swap(a[keyi], a[left]);keyi left;QuickSort1(a, left, keyi-1);QuickSort1(a, keyi1, right); }4.2.5 快速排序的非递归版本 虽然递归可能涉及栈溢出的情形但是一般深度到1万才会爆快速排序一般不会出现栈溢出的情况并且现在的编译器已经将递归优化的很好了。 但是学习快速排序的非递归形式可以对深度优先遍历和广度优先遍历有着更深层次的了解。 既然每次调用递归函数需要将子序列的左端和右端传入到函数参数接着开辟函数栈帧。那么为何不直接将这两个数直接放入栈这个数据结构中呢需要对该区间进行排序再出栈 思路如下 使用深度优先遍历首先将序列的首尾序号按照顺序先尾后首入栈此时如果栈不为空将栈顶两元素出栈并分割对应序列。将得到的左右子序列的首尾序号一共四个值按照顺序先尾后首先右后左入栈如果序列只有一个元素或者没有元素right left就不需要入栈重复步骤3直至栈为空排序完毕 代码实现 void QuickSortNorR(int*a, int left, int right) {// 创建栈 并按照先left后right的顺序入栈Stack stack;StackInit(stack);StackPush(stack, left);StackPush(stack, right);// 将栈顶两个元素出栈 进行一轮排序// 每次排序后 按照先右后左的顺序将区间的left和right入栈// 如果rightleft 不入栈while (!StackEmpty(stack)){int right StackTop(stack);StackPop(stack);int left StackTop(stack);StackPop(stack);// 这里使用挖坑法int keyi PartSort2(a, left, right);// 先右后左 先left后right入栈if (right keyi1){StackPush(stack, keyi1);StackPush(stack, right);}if (keyi-1 left){StackPush(stack, left);StackPush(stack, keyi-1);}}StackDestroy(stack); }当然也是可以使用队列的这样就是广度优先遍历类似于之前的二叉树层次遍历先将每一层处理好随后继续下一层。 使用栈先将左子序列排序完再排序右子序列。 使用队列先将左右子序列都分割一次后再进行下一次分割。 4.2.6 快速排序的分析 快速排序是递归的因此它的分析需要求解一个递推公式。其消耗空间容量与递归的最大深度一致。 我们对快速排序这样分析假设有一个随机的基准值不用三数中值分割法对一些小的文件也不用截止范围。 取 T ( 0 ) T ( 1 ) 1 T(0) T(1) 1 T(0)T(1)1, 快速排序的运行时间等于两个递归调用的运行时间加上花费在分割上的线形时间基准值的选取仅花费常数时间。我们得到基本的快速排序关系 T ( N ) T ( i ) T ( N − i − 1 ) c N T(N) T(i) T(N-i-1) cN T(N)T(i)T(N−i−1)cN 其中 i ∣ S 1 ∣ i|S_1| i∣S1​∣是 S 1 S_1 S1​中的元素个数 最坏情形的分析 基准值始终是最小元素。此时 i 0此时递推关系为 T ( N ) T ( N − 1 ) c N , N 1 T(N) T(N-1) cN, N1 T(N)T(N−1)cN,N1 反复递推可以得到 T ( N − 1 ) T ( N − 2 ) c ( N − 1 ) T(N-1) T(N-2) c(N-1) T(N−1)T(N−2)c(N−1) T ( N − 2 ) T ( N − 3 ) c ( N − 2 ) T(N-2) T(N-3) c(N-2) T(N−2)T(N−3)c(N−2) … … … T ( 2 ) T ( 1 ) c ( 2 ) T(2) T(1) c(2) T(2)T(1)c(2) 将以上所有方程相加得到 T ( N ) T ( 1 ) c ∑ i 2 N i O ( N 2 ) T(N) T(1) c\sum_{i2}^{N}{i} O(N^2) T(N)T(1)ci2∑N​iO(N2) 栈的深度为 O ( N ) O(N) O(N) 最好情形的分析 最好情况下每次的基准值都位于中间为了简化数学推导假设两个子序列刚好各为原数组的一半 T ( N ) 2 T ( N / 2 ) c N T(N) 2T(N/2) cN T(N)2T(N/2)cN 用N除去等式两边 T ( N ) N T ( N / 2 ) N / 2 c \frac{T(N)}{N} \frac{T(N/2)}{N/2} c NT(N)​N/2T(N/2)​c 反复套用得到 T ( N / 2 ) N / 2 T ( N / 4 ) N / 4 c \frac{T(N/2)}{N/2} \frac{T(N/4)}{N/4} c N/2T(N/2)​N/4T(N/4)​c T ( N / 4 ) N / 4 T ( N / 8 ) N / 8 c \frac{T(N/4)}{N/4} \frac{T(N/8)}{N/8} c N/4T(N/4)​N/8T(N/8)​c … … … T ( 2 ) 2 T ( 1 ) 1 c \frac{T(2)}{2} \frac{T(1)}{1} c 2T(2)​1T(1)​c 同样相加得到 T ( N ) N T ( 1 ) 1 c l o g N \frac{T(N)}{N} \frac{T(1)}{1} c logN NT(N)​1T(1)​clogN 由此得到 T ( N ) c N l o g N N O ( N l o g N ) T(N) cN logN N O(N logN) T(N)cNlogNNO(NlogN) 栈的深度为 O ( l o g N ) O(logN) O(logN) 平均情形的分析 时间复杂度为 O ( N l o g N ) O(N logN) O(NlogN),好在快速排序的平均情况下的运行时间与其最佳情况下的运行时间很接近而不是接近其最坏情况下的运行时间。 栈的深度为 O ( l o g N ) O(logN) O(logN) 稳定性在分割操作中若右端区间有两个关键字相同且均小于基准值则在交换到左端区间后它们的相对位置会发生变化即快速排序是一种不稳定的排序方法。 例如表 L{3,2,2}, 经过一趟排序后 L{2,2,3},两个2的相对次序已经发生了变化 注意在快速排序算法中并不产生有序子序列但每趟排序后会将上一趟划分的各个无序子表的基准值放到其最终的位置上。 快速排序的平均时间复杂度是最低的所以说快速排序是最综合的排序。 5. 选择排序 选择排序的思想是每一趟如第i趟在后面 n - 1 - i i 1, 2, 3, …, n-1个待排序元素中选取关键字最小的元素作为有序子序列的第 i 个元素直到 n-1 趟走完 此时还剩 1 个元素没有排序 就不用选了 5. 1 简单选择排序 根据上面的思想进行了一小段优化每一趟如第 i 趟取当前子序列的最小值和最大值将最小值放置子序列的第 i 个元素将最大值防置子序列的第(n - i 1)个元素直到只剩下一个元素这样经过 n/2 趟就可以将整个序列有序 代码如下 void SelectSort(int* a, int n) {// 每轮选出数组中最大数和最小数的下标// 将最大数放在数组最后 将最小数放在数组最前int begin 0;int end n - 1;while (begin end){int maxi begin;int mini begin;int i 0;// 找到最大和最小下标for (i begin1; i end; i){if (a[i] a[maxi]){maxi i;}if (a[i] a[mini]){mini i;}}// 交换 注意可能会出现最大或最小数已经被交换了的情况Swap(a[begin], a[mini]);if (maxi begin){maxi mini;}Swap(a[end], a[maxi]);begin;end--;} }这段代码需要注意的是如果首先将最小值放到了序列首元素位置而最大值恰好又在序列首元素位置这个时候需要进行判断如果最大值被换走了则修改最大值所在位置的下标 maxi 简单选择排序的性能分析 空间效率 仅使用常数个个辅助单元 空间复杂度为 O ( N ) O(N) O(N) 时间效率 T ( N ) N ( N − 2 ) ( N − 4 ) . . . 3 ( N 3 ) ( N − 3 2 1 ) 2 O ( N 2 ) T(N) N (N-2) (N-4) ...3 \frac{(N3)(\frac{N-3}{2} 1)}{2} O(N^2) T(N)N(N−2)(N−4)...32(N3)(2N−3​1)​O(N2) 稳定性假设序列 L {25351}经过一趟排序后变成 L {1 2 3 5 5}。显然5 和 5 的相对次序已经发生了变化。简单选择排序是一个不稳定的排序。 5. 2 堆排序 关于堆排序已经在之前文章中写明这里不过多赘述。 关于堆排序的详解 6. 归并排序 归并排序(MERGE-SORT)是建立在归并操作上的一种有效的排序算法, 该算法是采用分治法 (Divide and Conquer) 的一个非常典型的应用. 将已有序的子序列合并, 得到完全有序的序列;即先使每个子序列有序, 再使子序列间有序. 若将两个有序表合并成一个有序表, 称为二路归并. 由上图可知, 我们先要将当前的有序表长度为 1 或者 0(即有序)才可以进行归并.可以将一个有序表均分为两个子序列表,直至分到只剩长度为 1 或者 0, 再进行归并, 两两归并直至归并成一个长度为 n 的有序表. 与快速排序是前序不同, 归并排序按照这个思路其实是后序.先将序列的左右子序列排序后, 再将这两个子序列进行归并操作. 步骤如下: 动态申请一个长度为 n 的tmp表用来存放归并的结果将序列平分为左右两个子序列, 若左右子序列无序(长度不为 1 或者 0),继续按照先左后右的顺序平分直至分解到序列长度为1 或者 0,此时将左右子序列两两归并入 tmp 数组,再将 tmp 数组拷贝至原数组,返回上一层继续两两归并, 直至归并成一个长度为 n 的有序表 代码如下: void Merge(int* a, int* tmp, int left, int mid, int right) {int cur1 left; //数组1首元素下标int cur2 mid 1; //数组2首元素下表int index left; //tmp数组首元素下标while (cur1 mid cur2 right){//去两数组最小值尾插至tmp数组if (a[cur1] a[cur2]){tmp[index] a[cur1]; }else {tmp[index] a[cur2];}}while (cur1 mid){//如果数组1没有遍历完tmp[index] a[cur1];}while (cur2 right){//如果数组2没有遍历完tmp[index] a[cur2];}//拷贝tmp数组至a数组memcpy(aleft, tmpleft, sizeof(int) * (right - left 1)); }void _MergeSort(int* a, int* tmp, int left, int right) {if (right left)return;int mid (left right) / 2;//分解_MergeSort(a, tmp, left, mid); _MergeSort(a, tmp, mid1, right);//归并Merge(a, tmp, left, mid, right); }void MergeSort(int* a, int n) {// 先开辟一个tmp数组空间int* tmp (int*)malloc(sizeof(int) * n);if (tmp NULL){perror(malloc);}_MergeSort(a, tmp, 0, n-1); //[0, n-1]free(tmp); }归并排序的非递归版本 归并排序的非递归不能使用栈或者队列, 归并排序是后序的, 即使先两两归并完了, 还需要拿值, 这就会显得很复杂. 其实观看上图归并的区域, 我们可以不分解, 直接归并. 通过遍历数组, 调整每次子序列个数达到手动归并的效果.11归并-22归并-44归并…-得到长度为n的子序列 但是如果每次都是将序列长度乘以2, 除非 n 是 2 的幂次方, 否则肯定会出现越界的情况. 这就要求, 在两序列进行合并的时候, 先添加一些判断, 来判断两序列是否有下标越界的情况, 如果越界, 直接结束就可以了 代码实现: void MergeSortNorR(int* a, int n) {int* tmp (int*)malloc(sizeof(int) * n);if (tmp NULL){perror(malloc fail);}// 11归并-22归并-...int gap 1;int i 0;while (gap n){for (i 0; i n; i 2*gap){int mid i gap - 1;int left i;int right i 2*gap - 1;if (mid1 n){//如果右子序列的左下标越界直接返回break;}if (right n){//如果右子序列的右下标越界修改下标right n-1;}Merge(a, tmp, left, mid, right);}gap * 2;}free(tmp); }2路归并排序算法的性能分析如下 空间效率: 辅助空间为 n 个单元, 所以算法的空间复杂度为 O ( N ) O(N) O(N) 时间效率: 每趟归并的时间复杂度为 O ( N ) O(N) O(N), 共需进行 ⌊ l o g 2 N ⌋ \lfloor log_2N \rfloor ⌊log2​N⌋趟归并, 所以算法的时间复杂度为 O ( N l o g N ) O(N logN) O(NlogN) 稳定性: 归并操作不会改变相同关键词记录的相对次序, 所以2路归并排序算法是一种稳定的排序方法. 7. 计数排序 计数排序的思想很好理解 遍历数组统计每个数出现的次数, 存放到一个计数器数组, 计数器数组长度为原数组的范围遍历统计数组, 并按顺序重新放置原数组中 代码实现: void CountSort(int* a, int n) {int min a[0];int max a[0];int i 0;// 得到序列的最大最小值for (i 1; i n; i){if (a[i] min){min a[i];}if (a[i] max){max a[i];}}// 得到序列的范围int range max - min 1;printf(range:%d\n, range);int* tmp (int*)calloc(range, sizeof(int));if (tmp NULL){perror(malloc fail);}// 统计各数for (i 0; i n; i){tmp[a[i] - min];}// 从小到大重新copy到原数组int index 0;for (i 0; i range; i){while (tmp[i]){a[index] i min;tmp[i]--;}}free(tmp); }计数排序的性能分析 空间复杂度: 使用了计数器数组, 该数组长度为 range (原数组的范围), 空间复杂度为 O ( r a n g e ) O(range) O(range) 时间复杂度: 排序一共有三次循环: 得到原数组的最小值和最大值(循环n次), 构建计数器数组(循环n次),将计数器数组的计数反映到原数组中去(循环range次). 所以时间复杂度为 O ( m a x ( n , r a n g e ) ) O(max(n, range)) O(max(n,range)) 虽然计数排序的效率很高, 但是它的使用空间很小, 对于范围比较小的一串序列, 速度是十分惊人的. 8. 基数排序 基数排序是一种很特别的排序方法, 它不基于比较和移动进行排序, 而基于关键字各位的大小进行排序. 基数排序是一种借助多关键字牌序的思想对单逻辑关键字进行排序的方法 为实现多关键字排序, 通常由两种方法: 第一种是最高位优先(MSD), 按关键字位权重递减一次逐层划分成若干更小的子序列, 最后将所有子序列依次链接成一个有序序列.第二种是最低位优先(LSD), 按关键字位权重递增进行排序, 最后形成一个有序序列 步骤如下: 创建一个长度为10的队列数组如果数组中最小值为负数, 先将数组所有元素加上负数的绝对值得到数组中最大数, 并取到最大数的位数从个位-十位-百位-…-最高位, 依次按位权重排序每位排完后, 按顺序出队列, 返回原数组 代码实现: int getMax(int* a, int n) {int i 0;int max a[0];for (i 1; i n; i){if (a[i] max){max a[i];}}return max; }int getMin(int* a, int n) {int i 0; int min a[0];for (i 1; i n; i){if (a[i] min){min a[i];}}return min; }int getMaxDigit(int* a, int n) {// 得到最大数, 并取到其的位数int max getMax(a, n);int max_digit 1;while (max 10){max_digit;max / 10;}return max_digit; }int getDigitNum(int num, int base) {while (base 1){num / 10;base--;}return num % 10; }// 将数组所有元素变为非负数, 返回最小值 int arrayToNonnegative(int*a, int n) {int min getMin(a, n);int i 0;if (min 0){for (i 0; i n; i){a[i] - min ; }}return min; }//将数组恢复原来的状态 void arrayToPreliminary(int* a, int n, int min) {int i 0;if (min 0){for (i 0; i n; i){a[i] min;}} }void RadixSort(int* a, int n) {// 创建长度为10的队列数组Queue* count (Queue*)malloc(sizeof(Queue) * 10);int i 0;for (i 0; i 10; i){QueueInit(count[i]); }int min arrayToNonnegative(a, n);int max_digit getMaxDigit(a, n);// 1-个位 2-十位 3-百位int base 1;// 从个位到最高位, 如果某数该位是i, 入i队列while (base max_digit){for (i 0; i n; i){int digit_num getDigitNum(a[i], base); //得到每个数在该位的数字 QueuePush(count[digit_num], a[i]); //入对应下标序号的队列中}int index 0;for (i 0; i 10; i){//按升序将每个队列的元素重新放回数组中while (!QueueEmpty(count[i])){a[index] QueueFront(count[i]);QueuePop(count[i]);}}base;}arrayToPreliminary(a, n, min);// 释放数组空间for (i 0; i 10; i){QueueDestroy(count[i]);}free(count); }基数排序算法的性能分析 空间分析: 一趟排序需要的辅助存储空间为r(r个队列:r个队头指针和r个尾指针), 但以后的排序中会重复使用这些队列, 所以基数排序的空间复杂度为 O ( r ) O(r) O(r) 时间效率: 基数排序需要进行 d 趟分配和收集, 一趟分配需要 O ( N ) O(N) O(N), 一趟收集需要 O ( r ) O(r) O(r) , 所以基数排序的时间复杂度为 O d ( N r ) Od(Nr) Od(Nr) 稳定性: 对于基数排序算法而言, 很重要的一点就是按位排序时必须是稳定的. 因此, 这也保证了基数排序的稳定性. 9. 排序算法复杂度及稳定性总结 本章完.
http://www.zqtcl.cn/news/764282/

相关文章:

  • 河北省城乡和住房建设厅网站网店代运营托管
  • 彩票网站建设wordpress判断用户权限
  • 简洁大气企业网站源码h5商城网站建设是什么
  • 河间做网站价格wordpress评论导出
  • 网站关键词布局图网站推广与宣传怎么做
  • 小说类网站程序西安移动网站建设
  • 贵州高端网站建设网站做好了怎么做后台
  • 网站建设与管理 答案国外做免费的视频网站有哪些
  • 网站建设电脑端手机端企业网站建设需求调研表
  • 怎么做游戏网站google国际版
  • 学校网站建设发展规划线上推广的渠道有哪些
  • 公主岭网站建设seo网站推广技术
  • 网站建设一次crm管理
  • 电商网站设计公司优选亿企邦wordpress管理员头像
  • 医院做网站需要多少钱wordpress 模板 设计
  • 建设网站的规则建设公司网站的原则
  • 专业网站定制 北京龙泉驿网站seo
  • 网站标签是什么网站flash导入页
  • 城市网站建设摘要论文网站建设基本步骤包括哪些
  • 如何做招聘网站分析wordpress状态修改
  • 兰考网站建设微信运营是干嘛的
  • 网站ps照片怎么做的网站开发项目实训报告
  • 做流量网站it建设人才网
  • 杭州拱墅区网站建设推荐定制型网站建设
  • 网站建设需要达到什么样的效果上海营销网站推广多
  • 现代化公司网站建设长沙公司网站建立
  • 网站开发需要哪些人才辽宁奔之流建设工程有限公司网站
  • 做旅游产品的网站有哪些个人做搜索网站违法吗
  • 营销型网站的功能网站制作价钱多少
  • angularjs 网站模板工作感悟及心得