本章主要是算法知识的基础解说,介绍了循环不变式,几个简单的排序算法:插入排序、合并排序、冒泡排序等,以及递归分治算法等内容。
1、循环不变式
循环不变式主要用来说明算法的正确性,那么什么是循环不变式呢,事实上就是在循环过程中,一些元素数据必须保持的一些性质,比如在插入排序中,数组为A,必须保证三个性质:
(1) 初始化:在循环開始之前,循环不变式是成立的,即:A[0]是有序的,A[1...n-1]是无序的。
(2) 保持:在循环的某一次迭代開始之前,循环不变式是成立的,那么在此次迭代结束后依旧应该是成立的,即:A[0...i]是有序的,A[i+1...n-1]是无序的。
(3) 终止:当循环结束的时候,从循环不变式就能够得出我们的算法的正确性,即:整个数组A是有序的,排序成功。
插入排序
/* * 算法导论 第二章 插入排序 * 使用增量方法,依次遍历数组并逐个插入有序队列 * 插入时从后至前,找到不大于当前元素的第一个位置 * 将此位置后面的元素一次向后移动,将元素插入此位置 * 时间复杂度为O(n^2) */ #include <iostream> #include <ctime> using namespace std; void printArray(int arr[], int len) { for (int i=0; i<len; i++) { cout << arr[i] << " "; } cout << endl; } void insertionSort(int *arr, int len); void binaryInsertSort(int *arr, int len); int main() { int len = 12; int *arr = new int[len]; srand(time(NULL)); for (int i=0; i<len; i++) { arr[i] = rand() % 100; } cout << "原数组:" << endl; printArray(arr, len); insertionSort(arr, len); cout << "直接插入排序后的数组:" << endl; printArray(arr, len); for (int i=0; i<len; i++) { arr[i] = rand() % 100; } cout << "原数组:" << endl; printArray(arr, len); binaryInsertSort(arr, len); cout << "二分插入排序后的数组:" << endl; printArray(arr, len); return 0; } /* * 直接插入排序 * 时间复杂度为O(n^2) */ void insertionSort(int *arr, int len) { for (int i=1; i<len; i++) { int temp = arr[i]; int j = i - 1; while (j >= 0 && arr[j] > temp) { arr[j+1] = arr[j]; j = j - 1; } arr[j+1] = temp; } } /* * 二分插入排序 * 时间复杂度依旧为O(n^2) */ void binaryInsertSort(int *arr, int len) { //首先利用二分查找找到插入点 for (int i=1; i<len; i++) { int key = arr[i]; int low = 0; int high = i - 1; while (low < high) { int mid = (low + high) / 2; if (arr[mid] > key) { high = mid - 1; } else { low = mid + 1; } } //将插入点之后的元素依次向后移动一位 for (int j=i-1; j>low; j--) { arr[j+1] = arr[j]; } if (arr[low] > key) { arr[low+1] = arr[low]; arr[low] = key; } else { arr[low+1] = key; } } }
希尔排序
/* * 希尔排序 * 基本思想:先将整个集合,依据不同的步长分成若干个子集和,然后对各个子集和分别 * 进行直接插入排序,不断缩小步长直至1,及最后进行一次整个集合的直接插入排序 * 此算法也是属于一种插入排序算法 * 只是直接插入排序算法的时间复杂度为O(n^2),希尔排序时间复杂度要小得多 * 与直接插入排序的最大不同就是希尔排序在不断的提高所排序的集合总体基本有序性 * 这样就降低了直接插入排序中元素比較和移动的次数,从而降低了时间复杂度, * 最后一次对整个集合的直接插入排序,由于整个集合已经基本有序,所以时间复杂度会非常低 * 时间复杂度:希尔排序的时间复杂度与在排序过程中选择的增量序列(也就是步长序列)有关, * 增量序列一般应该满足:序列中值没有除1以外的公因子,而且最后一个增量至必须为1 * 较好的增量序列可使时间复杂度降为:O(n^(3/2)),甚至O(n^1.3) */ #include <iostream> #include <ctime> using namespace std; void shellSort(int *arr, int len, int *dlta, int t); void shellInsert(int *arr, int len, int dk); void printArray(int arr[], int len, char *str) { cout << str << endl; for (int i=0; i<len; i++) { cout << arr[i] << " "; } cout << endl; } int main() { srand(time(NULL)); int len = 15; int *arr = new int[len]; for (int i=0; i<len; i++) { arr[i] = rand() % 100; } int t = 4; int *dlta = new int[t]; for (int i=0; i<t; i++) { dlta[i] = pow(2.0, t-i)-1; } printArray(arr, len, "原数组"); shellSort(arr, len, dlta, t); printArray(arr, len, "希尔排序后数组"); delete[] dlta; delete[] arr; return 0; } void shellSort(int *arr, int len, int *dlta, int t) { for (int i=0; i<t; i++) { shellInsert(arr, len, dlta[i]); } } void shellInsert(int *arr, int len, int dk) { for (int i=dk; i<len; i++) { int key = arr[i]; int j = i - dk; while (j >= 0 && arr[j] > key) { arr[j+dk] = arr[j]; j -= dk; } arr[j+dk] = key; } }
2、在算法分析中,我们通常都主要考虑算法的时间复杂度,由于空间资源相对照较便宜,easy满足,而时间是眼下主要追求,并且是考虑算法的最坏情况下的时间复杂度,可是有时(高速排序),我们也会考虑平均情况下的时间复杂度,这样也是很有效的,由于通常平均情况下的时间复杂度仅仅与最坏情况下的时间复杂度在常系数上有差异。算法的时间复杂度的详细分析通常能够採用递归式求解(列出时间复杂度的表达式,猜想验证或者直接计算),递归树分析等。
3、分治法
非常多算法在结构上是递归的,算法一次有一次的调用自身来解决相关的子问题。这样的算法通常採用的就是分治策略:将原问题分成若干个较小的子问题,在递归的解决这些子问题,最后将自问题的解合并成原问题的解。主要分为三个步骤:
分解:将原问题分解成为一些列的子问题。
解决:递归的解决各个子问题,若子问题足够小,就直接解决。
合并:将子问题的解合并成原问题的解。
下面是合并排序以及 习题2-4 的逆序对数量计算
/* * 算法导论 第二章 合并排序 * 利用递归分治法 * 同一时候计算序列中逆序对数量 * 时间复杂度为O(nlgn) */ #include <iostream> using namespace std; //定义最大整数 #define MAX_NUMBER 0x7fffffff void printArray(int arr[], int len) { for (int i=0; i<len; i++) { cout << arr[i] << " "; } cout << endl; } void mergeSort(int arr[], int p, int r); void merge(int arr[], int p, int q, int r); //统计序列中元素的逆序对数量 int count; int main() { int arr[] = {12, 21, 9, 80, 3, 11, 90, 4, 67}; int len = sizeof(arr) / sizeof(arr[0]); cout << "原数组:" << endl; printArray(arr, len); mergeSort(arr, 0, len-1); cout << "合并排序后的数组:" << endl; printArray(arr, len); cout << "逆序对数量:" << count << endl; return 0; } void mergeSort(int arr[], int p, int r) { if (p < r) { int q = (p + r) / 2; mergeSort(arr, p, q); mergeSort(arr, q+1, r); merge(arr, p, q, r); } } void merge(int arr[], int p, int q, int r) { int n1 = q - p + 1; int n2 = r - q; int *left = new int[n1+1]; int *right = new int[n2+1]; for (int i=0; i<n1; i++) { left[i] = arr[p+i]; } for (int i=0; i<n2; i++) { right[i] = arr[q+1+i]; } left[n1] = right[n2] = MAX_NUMBER; int i = 0, j = 0; for (int k=p; k<=r; k++) { if (left[i] <= right[j]) { arr[k] = left[i]; i++; } else { /* 由于left数组元素都是在right的左边 * 所以一旦left[i]>right[j],则left[i]以后的元素都大于right[j] * 逆序对数量添加 n1-1-i+1=n1-i 对 */ count += n1 - i; arr[k] = right[j]; j++; } } delete[] left; delete[] right; }
4、冒泡排序
/* * 算法导论 第二章 冒泡排序 * 思想:冒泡排序,顾名思义就是每一次将序列中最小的元素(较轻)移到前面去 * 轻的往上冒,重的往下掉,每次将未排序部分的最轻元素冒到最上面 * 时间复杂度为O(n^2) */ #include <iostream> using namespace std; void printArray(int arr[], int len) { for (int i=0; i<len; i++) { cout << arr[i] << " "; } cout << endl; } int main() { int arr[] = {12, 21, 9, 80, 3, 11, 90, 4, 67}; int len = sizeof(arr) / sizeof(arr[0]); cout << "原数组:" << endl; printArray(arr, len); for (int i=0; i<len; i++) { for (int j=len-1; j>i; j--) { if (arr[j] < arr[j-1]) { int temp = arr[j]; arr[j] = arr[j-1]; arr[j-1] = temp; } } } cout << "冒泡排序后的数组:" << endl; printArray(arr, len); return 0; }
5、习题2.3-6 解答
不行,即使使用二分查找,在插入排序的时候,可以在O(lgn)时间内找到插入点,可是要将插入点之后的元素都向后移动一位,仍然须要O(n)的时间,所以插入排序的总体时间复杂度仍然是O(n^2)。
6、习题2.3-7 解答
/* * 算法导论 第二章 习题2.3-7 * 此题利用合并排序+二分查找法 * 利用合并排序法对数组进行排序时间为O(nlgn) * 对S中的每一个元素进行一次二分查找时间为O(n*lgn) * 所以总的时间复杂度为O(nlgn) */ #include <iostream> using namespace std; int binarySearch(int arr[], int low, int high, int x); void mergeSort(int arr[], int p, int r); void merge(int arr[], int p, int q, int r); void printArray(int arr[], int len) { for (int i=0; i<len; i++) { cout << arr[i] << " "; } cout << endl; } int main() { int S[] = {12, 21, 9, 80, 3, 11, 90, 4, 67}; int x = 22; int len = sizeof(S) / sizeof(S[0]); cout << "原数组S:" << endl; printArray(S, len); //利用合并排序法对S排序 mergeSort(S, 0, len-1); int i, index = -1; for (i=0; i<len; i++) { //利用二分查找S中是否存在x-S[i]的元素 index = binarySearch(S, 0, len-1, x-S[i]); if (index != -1 && index != i) { break; } } if (index != -1) { cout << "S中的元素:" << S[i] << " + " << S[index] << " = " << x << endl; } else { cout << "S中不存在和为" << x << "的两个元素" << endl; } return 0; } int binarySearch(int arr[], int low, int high, int x) { while (low <= high) { int middle = (low + high) / 2; if (x > arr[middle]) { low = middle + 1; } else if (x < arr[middle]) { high = middle - 1; } else { return middle; } } return -1; } void mergeSort(int arr[], int p, int r) { if (p < r) { int q = (p + r) / 2; mergeSort(arr, p, q); mergeSort(arr, q+1, r); merge(arr, p, q, r); } } void merge(int arr[], int p, int q, int r) { int n1 = q - p + 1; int n2 = r - q; int *left = new int[n1+1]; int *right = new int[n2+1]; for (int i=0; i<n1; i++) { left[i] = arr[p+i]; } for (int i=0; i<n2; i++) { right[i] = arr[q+1+i]; } left[n1] = right[n2] = 0x7fffffff; int i = 0, j = 0; for (int k=p; k<=r; k++) { if (left[i] <= right[j]) { arr[k] = left[i]; i++; } else { arr[k] = right[j]; j++; } } delete[] left; delete[] right; }