本文共 10141 字,大约阅读时间需要 33 分钟。
//generate a array of n elements, range [rangL, rangeR] int *generateRandomArray(int n, int rangL, int rangeR) { assert(rangeR >= rangL); int *arr = new int[n]; srand(time(NULL)); for (int i = 0; i < n; i++) { arr[i] = rand() % (rangeR - rangL + 1) + rangL; } return arr; } templatevoid showArray(T arr[], int n) { for (int i = 0; i < n; i++) { cout << arr[i] << " "; } cout << endl; }
生成随机的n个数量的数组,输出数组每一个元素的内容。测试排序算法使用的标准就是运行时间和排序的正确性,所以需要一个验证正确性和计算排序时间的:
templatevoid testSort( string sortName, void(*sort)(T[], int), T arr[], int n){ clock_t startTime = clock(); sort(arr, n); clock_t endTime = clock(); if (isSorted(arr, n)){ cout << sortName << ": " << double(endTime - startTime)/CLOCKS_PER_SEC << "s" << endl; } else cout << "sort function is wrong!" << endl; return; } template bool isSorted(T arr[], int n){ for (int i = 0; i < n-1; i++){ if (arr[i] > arr[i+1]) return false; } return true; }
这种算法应该是属于比较慢的算法了,编码简单,易于实现,是一些简单场景的选择。在一些特殊情况下,简单的排序算法更容易实现且更有效。从简单的算法演变到复杂的算法,循序渐进,作为子过程改进跟复杂的算法。
选择排序比较简单,如果是最简单的一个了。基本思想就是遍历每一个位置,选择这个位置之后最小的元素与当前元素对换:
templatevoid SelectionSort(T arr[], int n) { for (int i = 0; i < n; i++) { int minIndex = i; for (int j = i; j < n; j++) { if (arr[j] < arr[minIndex]) minIndex = j; } swap(arr[minIndex], arr[i]); }}
由于是模板类,所以还可以用来排序string,运算符重载还可以用来对类排序。
定义一个学生类用于排序:
struct student{ string name; float score; bool operator<(const student &otherStudent){ return score < otherStudent.score; } friend ostream& operator<<(ostream &os, const student &otherStudent){ os << "Student: " << otherStudent.name << " " << otherStudent.score << endl; return os; } };
插入排序也是一个的排序算法。
templatevoid InsertionSort(T arr[], int n){ for (int i = 1; i < n; i ++){ for (int j = i; j > 0; j --){ if (arr[j] < arr[j-1]){ swap(arr[j], arr[j-1]); } else break; } }}
template这次就正常了:void InsertionSort_version2(T arr[], int n){ for (int i = 1; i < n; i ++){ T temp = arr[i]; int j = 0; for (j = i; j > 0 && arr[j-1] > temp; j --){ arr[j] = arr[j-1]; } arr[j] = temp; }
对于插入排序还有一个比较好的性质,如果当前数组是一个基本有序的数组那么基本插入排序每一次就基本被终止了,那么就会很快。
在小数据量的情况下,两者的差别都不会特别大,但是当数据量变大的时候,两者的差别就会越来越大了。
template最主要的就是Merge函数了。对于temp数组是存在有偏移的,所以是需要减去一个偏移量,因为到最后l和r不一定是从0开始的。void __merge(T arr[], int l, int mid, int r) { T *temp = new T[r-l+1]; for (int i = l; i <= r; i++) { temp[i - l] = arr[i]; } int i = l, j = mid + 1; for (int k = l; k <= r; k++) { if (i > mid) { arr[k] = temp[j - l]; j++; } else if (j > r) { arr[k] = temp[i - l]; i++; } else if (temp[i - l] < temp[j - l]) { arr[k] = temp[i - l]; i++; } else { arr[k] = temp[j - l]; j++; } } delete[] temp;}template void __mergeSort(T arr, int l, int r) { if (l >= r) { return; } else { int mid = (l + r) / 2; __mergeSort(arr, l, mid); __mergeSort(arr, mid + 1, r); __merge(arr, l, mid, r); }}template void MergeSort(T arr[], int n) { __mergeSort(arr, 0, n - 1);}
可以看到对于一万个数组排序的时间很快,其实归并排序就是典型的用空间换时间的,每一次递归就开了一个空间,每一次递归的时候还有一个缓冲数组,所以使用的空间复杂度比一般的排序算法要大。
如果是十万个数据那么差距就更加大了:void __mergeSort(T arr, int l, int r) { if (l >= r) { return; } else { int mid = (l + r) / 2; __mergeSort(arr, l, mid); __mergeSort(arr, mid + 1, r); if (arr[mid] > arr[mid + 1]) { __merge(arr, l, mid, r); } }}
加上一个判断即可。
template可以看到代码量很少。void upMergeSort(T arr[], int n){ for (int sz = 1; sz <= n; sz = sz + sz){ for (int i = 0; i + sz < n; i += sz + sz){ __merge(arr, i, i+sz-1, min(i+sz+sz-1, n-1)); } }}
时间差不多。
归并排序和快速排序不同的就是,归并排序是无论什么情况直接就一分为二两部分,快速排序也是分成两部分,但是快速排序是先找到对应的一个元素的位置,然后直接找到这个元素应该在的位置:
templateint __partition(T arr[], int l, int r) { T v = arr[l]; int j = l; for (int i = l + 1; i <= r; i++) { if (arr[i] < v) { swap(arr[j + 1], arr[i]); j++; } } swap(arr[j], arr[l]); return j;}template void __quickSort(T arr[], int l, int r) { if (l >= r) { return; } int p = __partition(arr, l, r); __quickSort(arr, l, p - 1); __quickSort(arr, p + 1, r);}template void QuickSort(T arr[], int n) { __quickSort(arr, 0, n - 1);}
templateint __partition(T arr[], int l, int r) { swap(arr[l], arr[rand()%(r-l+1)+l]); T v = arr[l]; int j = l; for (int i = l + 1; i <= r; i++) { if (arr[i] < v) { swap(arr[j + 1], arr[i]); j++; } }
效果就不出了。
接下来还有一个问题,如果这个数组是一个基本有序的数组,测试一下数据:templateint __partition_version2(T arr[], int l, int r) { swap(arr[l], arr[rand() % (r - l + 1) + l]); T v = arr[l]; int i = l+1, j = r; while (true){ while (i <= r && arr[i] < v){ i++; } while (j >= l+1 && arr[j] > v){ j--; } if (i > j){ break; } else{ swap(arr[i], arr[j]); i++; j--; } } swap(arr[l], arr[j]); return j;}
templateint __partition_version3(T arr[], int l, int r){ swap(arr[l], arr[rand() % (r - l + 1) + l]); T v = arr[l]; int lt = l, gt = r+1, i = l+1; while (i < gt){ if (arr[i] < v){ swap(arr[i], arr[lt+1]); lt++; i++; } else if (arr[i] > v){ swap(arr[i], arr[gt-1]); gt--; } else{ i++; } } swap(arr[l], arr[lt]); return lt;}
堆是一种比较常用的数据结构,常用的堆有二叉堆,费波纳茨堆等等。堆是一颗完全二叉树,除了最后一层其他的每一层都是满的,而最后一层都的子节点都是集中在了左边。堆的实现在DataStructure里面提到,实现很简单。因为实现的是最大堆,出来的元素就是最大的:
using namespace std;templatevoid HeapSort_version1(T arr[], int n){ MaxHeap heap = MaxHeap (n); for (int i = 0; i < n; ++i) { heap.insert(arr[i]); } for (int j = n-1; j >= 0; --j) { arr[j] = heap.pop(); }}
但是有一个小问题,每一次都是插入这样效率不高,所以我们可以直接在原数组上操作
比如这就并不是一个正确的最大堆,需要用heapify转变一下。注意到每一个叶子都是一个堆,而且是符合规定的堆,所以肯定是从最底层开始。第一个不是堆的节点一定是最后一个叶子除2。所以第一个要做的就是对第一个不是堆的节点进行排序,很明显就是22这个元素了,对比交换,这个过程其实就是一个shitDown的过程,而我们要shitdown的元素是那5个不是堆的元素,所以对于这个数组直接做5次循环对每一个不是堆的元素进行shiftDown操作即可。
MaxHeap(item arr[], int n){ data = new item[n+1]; capacity = n; for (int i = 0; i < n; ++i) { data[i+1] = arr[i]; } count = n; for (int j = count/2; j >= 1; --j) { shiftDown(j); } }
之后就可以使用heapify来代替迭代插入,其实效果差别不大。堆排序在现实中其实蛮少用到的,常用的还是对于动态数据结构的维护。
templatevoid __shiftDown(T arr[], int n, int i){ while (2*i + 1 < n){ int change = 2*i + 1; if (change + 1 < n && arr[change] < arr[change+1]){ change ++; } if (arr[i] >= arr[change]){ break; } swap(arr[i], arr[change]); i = change; }}
接下了就是按照上述流程排序:
templatevoid HeapSort_version2(T arr[], int n) { //heapify,create a max heap; for (int i = (n-1)/2; i >= 0; --i) { __shiftDown(arr, n, i); } for (int j = n-1; j > 0 ; --j) { swap(arr[0], arr[j]); __shiftDown(arr, j, 0); }}
看看效果:
差别不是特别大,但是总的来说还是快了,因为它不需要进行额外的空间,也不需要赋值的操作等等,相对会快点。
平均时间复杂度 | 原地排序 | 额外空间 | 稳定性 | |
---|---|---|---|---|
Insertion Sort | 是 | 是 | ||
Merge Sort | 否 | 是 | ||
Quick Sort | 是 | 否 | ||
Heap Sort | 是 | 否 |
快速排序的额外空间是logn,因为递归的次数就是logn,而每一次递归就需要一个栈空间来存储。
最后附上GitHub代码:
转载地址:http://nkbxa.baihongyu.com/