算法导论小结(一)

用了几个月磕磕绊绊的总算把《算法导论》一书看完了,在此写篇博客总结一下学习到的知识。

首先先放上《算法导论》的思维导图:

由于本人的理解能力有限,故部分较难懂的内容没有加入到该思维导图中。

1.排序

排序问题是我们日常生活中经常遇到的一个问题,因此算法导论也把排序作为整个算法介绍的入门篇。在这么多排序算法里面,目前经典的排序算法有以下几种:

1.插入排序

对于少量元素的排序,插入排序是一个有效的算法。它的工作方式就像我们排序扑克牌一样,每次把一张新的扑克牌插入到已经排好序的序列中。假设排序序列的长度为n,由于插入排序每次加入一个新的元素都需要遍历几乎整个排好序的序列,故他的时间复杂度为O(n^2)。

以下为插入排序的Java代码:

[java] view plain copy

  1. public class Algorithm {
  2. public static void main(String[] args) {
  3. int A[] = {5, 2, 4, 6, 1, 3};
  4. InsertionSort(A);
  5. for(int num : A)
  6. System.out.println(num);
  7. }
  8. // 插入排序
  9. public static void InsertionSort(int A[]) {
  10. for (int i = 1; i < A.length; i++) {
  11. int key = A[i];
  12. int j = i-1;
  13. while(j >= 0 && A[j] > key) {
  14. A[j+1] = A[j];
  15. j--;
  16. }
  17. A[j+1] = key;
  18. }
  19. }
  20. }

插入排序还有一种变种,称为希尔排序,即Shell Sort,也称缩小增量排序,是直接插入排序算法的一种更高效的改进版本,是一种非稳定排序算法。希尔排序是把数组按下标的一定增量分组,对每组使用直接插入排序算法排序;随着增量逐渐减少,每组包含的元素越来越多,当增量减至1时,整个数组恰被分成一组,算法便终止。由于希尔排序的时间复杂度与增量的选取有关,在此不作深入讨论。

希尔排序的Java代码如下所示:

[java] view plain copy

  1. public class Algorithm {
  2. public static void main(String[] args) {
  3. int A[] = { 5, 2, 4, 1, 3, 6 };
  4. int ds[] = { 4, 2, 1 }; // 最后为1使得排序最终成功
  5. ShellSort(A, ds);
  6. for (int num : A)
  7. System.out.println(num);
  8. }
  9. /**
  10. * 希尔排序
  11. *
  12. * @param nums
  13. *            待排序数组
  14. * @param ds
  15. *            增量数组
  16. */
  17. public static void ShellSort(int nums[], int ds[]) {
  18. for (int d : ds) {
  19. // 分为d组
  20. for (int i = 0; i < d; i++) {
  21. // 插入排序
  22. for (int j = 0; j + d < nums.length; j += d) {
  23. int key = nums[j + d];
  24. int k = j;
  25. while (k >= 0 && key < nums[k]) {
  26. nums[k+d] = nums[k];
  27. k -= d;
  28. }
  29. nums[k+d] = key;
  30. }
  31. }
  32. }
  33. }
  34. }

2.冒泡排序

冒泡排序与插入排序类似,也是一种较为简单的排序算法。冒泡排序会重复地走访过要排序的数列,一次比较两个元素,如果他们的顺序错误就把他们交换过来,因此越大的元素会经由交换慢慢“浮”到数列的顶端,这就是这种排序方法命名的原因。由于需要两层循环遍历数组,所以冒泡排序的时间复杂度为O(n^2)。

以下为冒泡排序的Java代码:

[java] view plain copy

  1. public class Algorithm {
  2. public static void main(String[] args) {
  3. int A[] = {5, 2, 4, 6, 1, 3};
  4. BubbleSort(A);
  5. for(int num : A)
  6. System.out.println(num);
  7. }
  8. // 冒泡排序
  9. public static void BubbleSort(int A[]) {
  10. for (int i = 0; i < A.length; i++) {
  11. for (int j = 1; j < A.length - i; j++) {
  12. // 如果前面的元素比后面的大,则发生交换
  13. if (A[j-1] > A[j]) {
  14. int temp = A[j];
  15. A[j] = A[j-1];
  16. A[j-1] = temp;
  17. }
  18. }
  19. }
  20. }
  21. }

实际上面的冒泡排序代码还可以进行一点小优化,要是循环中没有发生交换则可直接退出:

[java] view plain copy

  1. // 冒泡排序(优化)
  2. public static void BubbleSortII(int A[]) {
  3. boolean swap = false; // 是否发生过交换
  4. for (int i = 0; i < A.length; i++) {
  5. swap = false;
  6. for (int j = 1; j < A.length - i; j++) {
  7. // 如果前面的元素比后面的大,则发生交换
  8. if (A[j-1] > A[j]) {
  9. swap = true;
  10. int temp = A[j];
  11. A[j] = A[j-1];
  12. A[j-1] = temp;
  13. }
  14. }
  15. // 没有发生过交换,已经排序完毕,可跳出循环
  16. if (swap == false)
  17. break;
  18. }
  19. }

3.归并排序

要提到归并排序就不得不讲到分治法(Divide and Conquer),分治法的思想是:把原问题分解成为几个规模较小,但类似于原问题的子问题,递归的去求解这些子问题,然后再合并这些子问题的解来建立原问题的解。归并排序就是是采用分治法的一个非常典型的应用。它是建立在归并操作上的一种有效的排序算法,该算法将已有序的子序列合并,得到完全有序的序列。用扑克牌来举例:在排序一副扑克牌的时候,我们现将其分成两叠大小相近的扑克牌,分别排序,然后我们再把这两叠已经排好序的扑克牌合并成一副更大的排好序的扑克牌,此时只需要每次比较两叠扑克牌的第一张牌即可。归并排序的图解如下:

设序列中有N个元素需要排序,则由上图易得可以把排序过程分为logN(以2为低的对数)次处理,每次循环N次,故归并排序的时间复杂度为O(N*logN)。

归并排序的Java代码如下:

[java] view plain copy

  1. public class Algorithm {
  2. public static void main(String[] args) {
  3. int A[] = {5, 2, 4, 6, 1, 3};
  4. MergeSort(A, 0, A.length-1);
  5. for(int num : A)
  6. System.out.println(num);
  7. }
  8. public static void MergeSort(int A[], int start, int end) {
  9. if (start < end) {
  10. // 中点
  11. int mid = (start+end)/2;
  12. // 子序列分别排序
  13. MergeSort(A, start, mid);
  14. MergeSort(A, mid+1, end);
  15. // 合并
  16. // 把子序列存到新数组中
  17. int leftLen = mid-start+1, rightLen = end-mid;
  18. int leftCounter = 0, rightCounter = 0, numCounter = start;
  19. int L[] = new int[leftLen], R[] = new int[rightLen];
  20. for (int i = 0; i < leftLen; i++)
  21. L[i] = A[start+i];
  22. for (int i = 0; i < rightLen; i++)
  23. R[i] = A[mid+1+i];
  24. // 比较子序列第一项元素
  25. while(leftCounter < leftLen && rightCounter < rightLen) {
  26. if(L[leftCounter] < R[rightCounter])
  27. A[numCounter++] = L[leftCounter++];
  28. else
  29. A[numCounter++] = R[rightCounter++];
  30. }
  31. // 把剩余的子序列加到后面
  32. while(leftCounter < leftLen)
  33. A[numCounter++] = L[leftCounter++];
  34. while(rightCounter < rightLen)
  35. A[numCounter++] = R[rightCounter++];
  36. }
  37. }
  38. }

更新非递归版本的归并排序:

[java] view plain copy

  1. public class Algorithm {
  2. public static void main(String[] args) {
  3. int A[] = { 5, 2, 4, 1, 3, 6};
  4. MergeSort(A);
  5. for (int num : A)
  6. System.out.println(num);
  7. }
  8. public static void MergeSort(int A[]) {
  9. int len = A.length;
  10. int temp[] = new int[len];
  11. int leftMin, leftMax, rightMin, rightMax; // leftMin ~ leftMax, rightMin
  12. // ~ rightMax
  13. for (int i = 1; i < len; i *= 2) {
  14. leftMin = leftMax = rightMin = rightMax = 0;
  15. while (leftMin < len) {
  16. rightMin = leftMax = leftMin + i;
  17. rightMax = rightMin + i;
  18. if (rightMax > len)
  19. rightMax = len;
  20. if (rightMin > rightMax)
  21. leftMax = rightMin = rightMax;
  22. int counter = 0;
  23. while (leftMin < leftMax && rightMin < rightMax)
  24. temp[counter++] = A[leftMin] > A[rightMin] ? A[rightMin++]
  25. : A[leftMin++];
  26. while (leftMin < leftMax)
  27. A[--rightMin] = A[--leftMax];
  28. while (counter > 0)
  29. A[--rightMin] = temp[--counter];
  30. leftMin = rightMax;
  31. }
  32. }
  33. }
  34. }

4.堆排序

首先我们来看看什么是堆:

如上图所示(二叉)堆是一个数组,它可以被看成一个近似的完全二叉树,树上每一个结点对应数组中的一个元素。除了最底层外,该树是完全充满的,而且是从左到右填充。在堆中,给定一个结点下标i(对于起始下标为1而言),则它的父节点为i/2,它的左孩子下标为i*2,右孩子下标为i*2+1。堆中结点的高度被定义为该结点到叶结点的最长简单路径,由数学公式可得含N个元素的堆高度为logN。

二叉堆可以分为两种形式:最大堆和最小堆,他们除了满足堆的基本性质外,最大堆满足:除了根结点外,所有结点的值小于等于父节点,最小堆反之。在堆排序算法中,我们使用最大堆,最小堆通常用于构造优先队列。

以下为Java的堆排序:

[java] view plain copy

  1. public class Algorithm {
  2. public static void main(String[] args) {
  3. int A[] = { 5, 2, 4, 6, 1, 3 };
  4. HeapSort(A);
  5. for (int num : A)
  6. System.out.println(num);
  7. }
  8. public static void HeapSort(int A[]) {
  9. BuildMaxHeap(A);
  10. for (int i = A.length - 1; i > 0; i--) {
  11. // 把根结点和最后的结点对调
  12. int temp = A[i];
  13. A[i] = A[0];
  14. A[0] = temp;
  15. // 对根结点进行最大堆性质维护
  16. MaxHeapify(A, i, 0);
  17. }
  18. }
  19. /**
  20. * 建立最大堆
  21. *
  22. * @param A
  23. *            数组
  24. */
  25. private static void BuildMaxHeap(int A[]) {
  26. int heapSize = A.length;
  27. // heapSize/2 ~ heapSize-1 均为叶结点,对非叶结点调用维护最大堆性质方法即可
  28. for (int i = heapSize / 2 - 1; i >= 0; i--)
  29. MaxHeapify(A, heapSize, i);
  30. }
  31. /**
  32. * 维护最大堆的性质,调整下标为i的结点位置
  33. * @param A 数组
  34. * @param heapSize 堆大小
  35. * @param index 结点下标
  36. */
  37. private static void MaxHeapify(int A[], int heapSize, int index) {
  38. int left = index*2+1, right = index*2+2, largest = index;
  39. // 选取父结点,左结点,右结点中值最大的当父结点
  40. if (left < heapSize && A[left] > A[index])
  41. largest = left;
  42. if (right < heapSize && A[right] > A[largest])
  43. largest = right;
  44. // 若子结点充当了父结点,对子结点递归调用方法维护最大堆性质
  45. if (largest != index) {
  46. int temp = A[largest];
  47. A[largest] = A[index];
  48. A[index] = temp;
  49. MaxHeapify(A, heapSize, largest);
  50. }
  51. }
  52. }

首先来看看MaxHeapify方法,该方法是用于维护最大堆性质的方法。若方法调整的结点发生了交换,则对其子结点递归的调用该方法继续维护最大堆性质,故该方法的调用次数与堆的高度有关,时间复杂度为O(h) = O(logN)。

再来看看BuildMaxHeap方法,该方法用于把一个无序的数组构造成一个最大堆。该方法自底向上对非叶结点调用MaxHeapify,咋看其时间复杂度为O(N*logN),但由数学推导可得其紧确时间复杂度为线性时间,此处不给出证明。

最后再来看HeapSort方法,堆排序首先把一个数组构造成最大堆,然后每次让堆的根结点(堆最大的元素)和堆最后的结点交换,并减少堆的大小,然后再对根结点调用MaxHeapify方法调整其位置。堆排序总共调用了N次MaxHeapify方法,故其时间复杂度为O(N*logN)

5.快速排序

快速排序也被称为霍尔排序,虽然快速排序的最坏时间复杂度为O(N^2),但是快速排序通常是实际排序应用中最好的选择,因为他的平均性能很好,期望时间复杂度为O(N*lgN)。快速排序与归并排序类似,都使用了分治思想。快速排序每次从数组中选择一个元素作为主元,把比主元小的元素放在其前面,把比主元大的元素方法主元的后面,然后再对其前后两个子数组进行相同的操作。

快速排序的Java代码如下所示:

[java] view plain copy

  1. public class Algorithm {
  2. public static void main(String[] args) {
  3. int A[] = {5, 2, 4, 6, 1, 3};
  4. QuickSort(A, 0, A.length-1);
  5. for(int num : A)
  6. System.out.println(num);
  7. }
  8. public static void QuickSort(int A[], int start, int end) {
  9. if (start < end) {
  10. // 主元
  11. int key = A[end];
  12. int i = start-1;
  13. for (int j = start; j < end; j++) {
  14. // 比key小的数放在前面
  15. if (A[j] < key) {
  16. i++;
  17. int temp = A[j];
  18. A[j] = A[i];
  19. A[i] = temp;
  20. }
  21. }
  22. i++;
  23. A[end] = A[i];
  24. A[i] = key;
  25. // 对子数组进行同样的操作
  26. QuickSort(A, start, i-1);
  27. QuickSort(A, i+1, end);
  28. }
  29. }
  30. }

上面的代码固定选取当前数组的最后一个元素作为主元,如果想要快速排序的平均性能更好,可以随机选取数组中的元素作为主元来减少出现最坏情况的概率。

时间: 2024-08-02 07:02:11

算法导论小结(一)的相关文章

定位算法及算法导论小结

一.电子围栏定位算法: 还是决定不做定位算法了,原因有下: 1.文献[1]中利用线性算法解决了TDOA问题(四个观测点以上),文献[2]中将AOA算法的形式也纳入进来.多个直线的交点就是待测点的位置.如果考虑单点是否在围栏内部,之前做的假设是,定位单点的算法复杂度高,但这两篇文献中说明的是:理论上是线性的,很简单.根据四个及以上观测量可将问题变成线性问题求解的后续扩展思路是,1结合新的应用场景和实际数据,得到算法应用的结果测试,像文献[3]就在车联网中应用了这个定位算法,但是实测数据我现在很难拿

算法导论6:排序小结和最值取法 2016.1.6

今天想做测试各个排序算法运行时间比较的程序,来对这几天学的排序算法小结一下.所以我先生成了1000000个1~150之间的随机数存到文件里.然后做了一个测试运行时间的程序.想看一下结构.但是结果效果并不太好.实践中,自己做的qsort函数和mergesort函数并没有理想中的那么快. 结果是这样:(可能并不准确,但却是是运行结果) 库函数快速排序:0.139000 seconds自制快速排序:0.375000 seconds归并排序:0.358000 seconds堆排序:0.525000 se

《算法导论》图相关算法小结

最近又抽空读了一遍<算法导论>,关于图的内容贯穿了多个章节(比如在动态规划一章埋了无权最短路径的伏笔,后面才专门讲),适用条件各异,而且都有证明过程. 如果不打算熟记证明,仅仅是应用,遇到具体场景再去回忆适用于哪种算法不太方便. 以下内容以手头的机械工业出版社基于原书第2版的译本整理了一下,便于速查. 不包含思考题.标注为"*"的章节和习题内容. 符号定义 一般地, 图G=(V, E),其中V代表顶点集合,E代表边集合.ω(u, v)代表从顶点u到顶点v的边的权值. 如果ω

红黑树&mdash;&mdash;算法导论(15)

1. 什么是红黑树 (1) 简介     上一篇我们介绍了基本动态集合操作时间复杂度均为O(h)的二叉搜索树.但遗憾的是,只有当二叉搜索树高度较低时,这些集合操作才会较快:即当树的高度较高(甚至一种极端情况是树变成了1条链)时,这些集合操作并不比在链表上执行的快.     于是我们需要构建出一种"平衡"的二叉搜索树.     红黑树(red-black tree)正是其中的一种.它可以保证在最坏的情况下,基本集合操作的时间复杂度是O(lgn). (2) 性质     与普通二叉搜索树不

《算法导论》读书笔记(五)

摘要: 本章介绍了二叉查找树的概念及操作.主要内容包括二叉查找树的性质,如何在二叉查找树中查找最大值.最小值和给定的值,如何找出某一个元素的前驱和后继,如何在二叉查找树中进行插入和删除操作.在二叉查找树上执行这些基本操作的时间与树的高度成正比,一棵随机构造的二叉查找树的期望高度为O(lgn),从而基本动态集合的操作平均时间为θ(lgn). 1.二叉查找树 二叉查找树是按照二叉树结构来组织的,因此可以用二叉链表结构表示.二叉查找树中的关键字的存储方式满足的特征是:设x为二叉查找树中的一个结点.如果

算法导论学习之插入排序+合并排序

最近准备花时间把算法导论详细的看一遍,强化一下算法和数据结构的基础,将一些总结性的东西写到博客上去. 一.插入排序 算法思想:如果一个数组A,从A[1–n-1]都是有序的,然后我们将A[n]插入到A[1–n-1]的某个合适的位置上去那么就可以保证A[1–n]都是有序的.这就是插入排序的思想:具体实现的时候我们将数组的第一个元素看出有序,然后从第二个元素开始按照上面的步骤进行插入操作,直到插入最后一个元素,然后整个数组都是有序的了. 时间复杂度分析:代码中有两重for循环,很容易看出时间复杂度是n

算法导论——lec 13 贪心算法与图上算法

之前我们介绍了用动态规划的方法来解决一些最优化的问题.但对于有些最优化问题来说,用动态规划就是"高射炮打蚊子",采用一些更加简单有效的方法就可以解决.贪心算法就是其中之一.贪心算法是使所做的选择看起来是当前最佳的,期望通过所做的局部最优选择来产生一个全局最优解. 一. 活动选择问题 [问题]对几个互相竞争的活动进行调度:活动集合S = {a1, a2, ..., an},它们都要求以独占的方式使用某一公共资源(如教室),每个活动ai有一个开始时间si和结束时间fi ,且0 ≤ si &

算法导论--图的遍历(DFS与BFS)

转载请注明出处:勿在浮沙筑高台http://blog.csdn.net/luoshixian099/article/details/51897538 图的遍历就是从图中的某个顶点出发,按某种方法对图中的所有顶点访问且仅访问一次.为了保证图中的顶点在遍历过程中仅访问一次,要为每一个顶点设置一个访问标志.通常有两种方法:深度优先搜索(DFS)和广度优先搜索(BFS).这两种算法对有向图与无向图均适用. 以下面无向图为例: 1.深度优先搜索(DFS) 基本步骤: 1.从图中某个顶点v0出发,首先访问v

算法导论8:数据结构——栈 2016.1.8

栈在暑假的时候接触过了,当时还写了个计算器,用的中缀表达式后缀表达式的栈操作. http://www.cnblogs.com/itlqs/p/4749998.html 今天按照算法导论上的讲解规范了一下代码.主要是栈的初始化.判断空栈.入栈.出栈.遍历栈. #include<stdio.h> #define MAXTOP 10 struct _stack { int top; int num[MAXTOP+1]; }s; void init(struct _stack &S) { S.