1. 冒泡排序(Bubble Sort)
1.1 算法描述
(1)比较相邻元素,如果第一个比第二个大,则交换这两个元素;
(2)对每个相邻的元素做同样的比较,从开始第一对到结尾的最后一对,每次比较并交换结束,本次的最后一个元素是最大的数(大的下沉);
(3)重复对所有元素做以上的操作(比较,交换),除了每次交换玩的最后一个元素(已经是最大了),直到排序完成。
Note:先确定尾部元素。
1.2 算法实现
#include<iostream> #include<vector> using namespace std; template <typename T> vector<T> BubbleSort(vector<T> series) { if (series.empty()) { return series; } bool flag; for (int i = 0; i < series.size() - 1 && flag; i++) { flag = flase; for (int j = 0; j < series.size() - 1 - i; j++) { if (series[j] > series[j + 1]) { flag = true; //如果序列是有序的,就不会执行交换,flag==false,则外层for直接跳出不再执行。 swap(series[j],series[j+1]); } } } return series; } int main() { int ser; vector<int>series; while (cin>>ser) { series.push_back(ser); } cout << "排序前:" << endl; for (auto it = series.begin(); it != series.end(); it++) { cout << *it << "\t"; } cout << endl; vector<int>seriesSorted; seriesSorted = BubbleSort(series); cout << "排序后:" << endl; for (auto it = seriesSorted.begin(); it != seriesSorted.end(); it++) { cout << *it << "\t"; } getchar(); return 0; }
1.3 算法分析
(1)冒泡排序时间复杂度平均为O(n^2n2)
(2)最坏时间复杂度为O(n^2n2)
(3)最好时间复杂度为O(n),在内层循环中加一个标志位,每次遍历外循环的时候判断标志位,如果一次都没有发生交换的话,就说明序列是有序的,只用执行N-1次即可。
(4)也就是当序列是有序时
(5)空间复杂度为O(1)
(6)算法稳定
2.选择排序(Selection Sort)
2.1 算法描述
(1)对未排序的序列寻找最小(大)元素,与本次参与排序序列的起始位置交换;
(2)对剩余未排序的元素继续进行(1)的操作,直到所有元素排序完毕。
2.2 算法实现
template <typename T> vector<T> SelectSort(vector<T> series) { int minIndex; for (int i = 0; i < series.size() - 1; i++) { minIndex = i; for (int j = i + 1; j < series.size(); j++) { if (series[j] < series[minIndex]) { minIndex = j; } } swap(series[i],series[minIndex]); } return series; }
2.3 算法分析
(1)选择排序时间复杂度平均为O(n^2n2)
(2)最坏时间复杂度为O(n^2n2),不管序列有序还是无序,都要将两个内外循环跑完。因为每一次循环都只能确定一个最大(最小)值,而无法知道整个序列是否已经有序。
(3)最好时间复杂度为O(n^2n2)
(4)空间复杂度为O(1)
(5)算法不稳定,假如序列为5 5 8 2 9,则第一个5会和2发生交换,也就是被交换到第二个5个后面,所以不稳定。
3.插入排序
3.1算法描述
(1)以序列的第一个元素作为已排序完成的序列开始;
(2)取出
下一个元素,在已经排好序的序列中由后往前进行一一比较;
(3)对比时,若取出的元素较小,则将已排好序的序列中的此元素向后移动
;
(4)重复步骤(3),直到取出的元素大于或等于前面排好序的序列元素,则将取出的元素插入到该位置上;
(5)重复步骤(1)~(4),直到排序完成。
3.2 算法实现
template <typename T> vector<T>InsertSort(vector<T> series) { for (int i = 1; i < series.size(); i++) { int preIndex = i - 1; T current = series[i];//取出需要插入的元素 while (preIndex >= 0 && current < series[preIndex]) { series[preIndex + 1] = series[preIndex];//将序列中值较大的后移 preIndex--; } series[preIndex + 1] = current; } return series; }
3.3 算法分析
(1)插入排序时间复杂度平均为O(n^2n2)
(2)最坏情况,当序列完全无序时,时间复杂度为O(n^2n2)
(3)最好情况,当序列为递增(递减)顺序时,时间复杂度为O(n)。因为当序列有序时,while条件均不可能满足,内层的循环次数均为0,所以只将外循环for跑一遍即可。
(4)空间复杂度为O(1)
(5)算法稳定
4.希尔排序
4.1 算法描述(Shell Sort)
希尔排序又称为缩小增量排序,是插入排序的改进版本,基于“原始数据元素集合越有序,直接插入排序算法的时间效率越高”思想。
该方法的基本思想是:设排序元素序列有n个元素,首先取一个整数increase(小于n)作为间隔将全部元素分为increase个子序列,所有距离increase的元素放在同一个子序列中,在每个序列中分别进行插入排序。然后缩小间隔increase,重复上述子序列划分和排序工作,直到increase = 1,将所有元素放在同一个子序列中排序为止。
原因:因为刚开始,increase的取值较大,每个子序列中的元素较少,排序速度快。到了排序后期,increase取值逐渐缩小,子序列中元素的个数逐渐增多,但由于前面工作的基础,大多数元素已经基本有序,所以排序速度仍然很快。
4.2 算法实现
#include<iostream> #include<vector> using namespace std; void shellSort(vector<int>&arr) { int increase = arr.size() / 2; while (increase >=1) { for (int i = increase; i < arr.size(); i++) { int current = arr[i]; int preIndex = i - increase; while (preIndex >= 0&&arr[preIndex]>current) { arr[preIndex + increase] = arr[preIndex]; preIndex -= increase; } arr[preIndex + increase] = current; } increase = increase / 2; } } int main() { vector<int>arr = { 65,34,25,87,12,38,56,46,14,77,92,23 }; shellSort(arr); for (int i = 0; i < arr.size(); i++) { cout << arr[i] << "\t"; } system("pause"); return 0; }
4.3 算法分析
(1)希尔排序的时间复杂度平均为O(n^1.3)~O(n2),看增量的设置。—>这个不会分析。
(2)最坏情况时间复杂度为O(n^2n2)
(3)最好情况时间复杂度为O(n),与直接插入排序一样,当序列有序时,内层while不会执行。
(4)空间复杂度为O(1)
(5)算法不稳定,按照增量进行插入,会导致相同元素相对位置发生改变。
5.堆排序
5.1 完全二叉树
5.1.1 定义
对于一个树高为h的二叉树,如果其第0层至第h-1层的节点都满。如果最下面一层节点不满,则所有的节点在左边的连续排列,空位都在右边。这样的二叉树就是一棵完全二叉树。
5.1.2 性质
如果具有n个节点的完全二叉树按照层次并按从左到右的顺序从0开始编号,则:
(1)序号为0的节点是根
(2)对于i>0,其父节点的编号为(i-1)/2。
(3)若2i+1<n,其左子节点的序号为2i+1,否则没有左子节点。
(4)若2i+2<n,其右子节点的序号为2i+2,否则没有右子节点。
5.2 堆
在满足完全二叉树性质的基础上,根结点的键值时所有堆结点的最大值,且每个结点的值都比其孩子结点大,为最大堆,而最小堆反之。另外,最大堆和最小堆各结点的左右孩子大小随意。
5.3 构建最大堆
(1)首先找到最后一个叶子节点的父节点,比较父节点与子节点,若父节点小于其中一个子节点,则与该子节点交换位置,执行(2);若父节点大于任意子节点,则不用交换,则直接从这个父节点继续往左遍历检查是否符合最大堆的条件,若不符合,则继续执行(1)的操作。
(2)若父节点与其子节点交换位置,则可能会影响以子节点为根的原最大堆,因此需要检查以子节点为根的堆是否还是最大堆,若不是,则调整。(这一步很重要)
5.4 堆排序
5.4.1 堆排序思路
(0)堆排序是针对于直接选择排序做出的改进。共同点是选择排序的和堆排序都需要n-1趟才能排序结束且一趟只能确定一个最大(小)关键字。不同点是选择排序每一趟确定出最大(小)关键字的时间复杂度为O(n),但堆排序每一趟确定最大(小)关键字的时间复杂度为O(log2n)。
(1)将初始待排序关键字序列(R1,R2…Rn)构建成大顶堆,此堆为初始的无序区;
(2)将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,…Rn-1)和新的有序区(Rn)
(3)由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,…Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2…Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。
5.4.2 堆排序实现
#include<iostream> #include<vector> using namespace std; //调整最大堆 //arr数据序列 i需调整的结点 len序列长度(因为每次排序都会减1,所以长度动态变化) void adjustHeap(vector<int>&arr, int i, int len) { int temp = arr[i]; for (int j = 2 * i + 1; j < len; j = 2 * i + 1) { if (j + 1 < len && arr[j + 1] > arr[j]) { j++; } if (temp < arr[j]) { arr[i] = arr[j]; i = j; } else { break; } } arr[i] = temp; } //初始化最大堆 void InitHeap(vector<int>&arr) { for (int i = (arr.size() - 1) / 2; i >= 0; i--) { adjustHeap(arr, i, arr.size());//从下向上构建大顶堆 } } //堆排序 void heapSort(vector<int>&arr) { InitHeap(arr);//初始化大顶堆 for (int i = arr.size() - 1; i > 0; i--) { swap(arr[0], arr[i]);//将堆顶元素放到序列尾部 adjustHeap(arr, 0, i);//从上到下重新调整为大顶堆 } } //测试 int main() { //int len; //cin >> len; //vector<int>arr; //for (int i = 0; i < len; i++) //{ // int temp; // cin >> temp; // arr.push_back(temp); //} vector<int>arr = { 10,50,32,5,76,9,40,88 }; heapSort(arr); for (auto it = arr.begin(); it != arr.end(); it++) { cout << *it << "\t"; } system("pause"); return 0; }
5.4.3 算法分析
(1)堆排序时间复杂度平均为O(nlog2n)。每次调整大顶堆的时间复杂度为O(log2n)且只能得到一个最大(小)值,排好序需要n趟同样的调整操作,因此堆排序的时间复杂度为O(log2n)。
(2)最坏情况时间复杂度为O(nlog2n)
(3)最好情况时间复杂度为O(nlog2n)
(4)空间复杂度为O(1)
(5)算法不稳定。
6.归并排序
6.1算法思路
归并排序可以分为两个部分来解决,第一部分为分,即将整个序列分成若干个子序列进行排序,从而得到若干个有序子序列;第二个部分为合,即合并分开的若干有序子序列。
6.2 算法实现
#include<iostream> #include<vector> using namespace std; void merge(vector<int>&arr, int left, int mid, int right) { vector<int>arrSort; int i = left; int j = mid + 1; while (i <= mid && j <= right) { if (arr[i] < arr[j]) { arrSort.push_back(arr[i]); i++; } else { arrSort.push_back(arr[j]); j++; } } while (i <= mid) { arrSort.push_back(arr[i]); i++; } while (j<=right) { arrSort.push_back(arr[j]); j++; } for (int i = left; i <= right; i++) { arr[i] = arrSort[i - left]; } } void mergeSort(vector<int>&arr, int left, int right) { if (arr.empty() || left >= right) return; int mid = (left + right) / 2; mergeSort(arr, left, mid); mergeSort(arr, mid + 1, right); merge(arr, left, mid, right); } int main() { vector<int>arr = { 7,1,3,8,12,11,2,9 }; //vector<int>arr = { 4,5,6,7,0,1,2,3 }; //merge(arr, 0, 3, 7); mergeSort(arr, 0, arr.size() - 1); for (auto it = arr.begin(); it != arr.end(); it++) { cout << *it << "\t"; } getchar(); return 0; }
6.3 算法分析
(1)将待排序列进行递归二分,直到分到一个元素为一个序列时终止二分,然后开始选择排序并合并。其中递归划分的时间复杂度为O(log2n),被划分的序列排序合并的时间复杂度为O(n),因此,总的时间复杂度为O(nlog2n)。因为不管序列呈何种状态(正序,到序,乱序),均要进行二分和合并的过程,所以最好最坏时间复杂度均为O(nlog2n)。
(2)因为在每个子序列中,只有符合调换条件,才发生调换,因此算法稳定。
7.快速排序
7.1算法思路
在待排序序列的任意位置确定一个基准数,经过一趟排序,将整段序列分为两部分,其中一部分小于基准数,另一部分大于基准数。然后继续对两部分使用排序,从而使整个序列有序。
7.2 算法实现
#include<iostream> #include<vector> using namespace std; void quickSort(vector<int>&arr, int left, int right) { if (left >= right) return; int k = arr[left]; int i = left; int j = right; while (i != j) { while (i < j && k <= arr[j]) { j--; } swap(arr[i], arr[j]); while (i < j && k >= arr[i]) { i++; } swap(arr[i], arr[j]); } quickSort(arr, left, i - 1); quickSort(arr, i + 1, right); return; } int main() { vector<int>arr = { 7,1,3,8,12,11,2,9 }; quickSort(arr, 0, 7); for (int i = 0; i < arr.size(); i++) { cout << arr[i] << " "; } getchar(); return 0; }
7.3 算法分析
(1)快速排序的时间复杂度平均为O(nlogn)。
(2)最坏情况时间复杂度为O(n^2n2),最坏的情况就是序列有序时,会使快排的过程退化成一棵二叉退化树(单分支二叉树),深度为n;且每次寻找基准数的时间为O(n)。因此最坏情况下快排算法的时间复杂度为O(n^2n2)。
(3)最好情况时间复杂度为O(nlogn),最好的情况就是每次选取的基准数都能够均分两个子数组的长度,这样快排的过程是一个完全二叉树结构,这是分解的长度就是完全二叉树的深度log2n;且每次寻找基准数的时间O(n),因此最好情况下的快速排序算法的时间复杂度为O(nlog2n)。
(4)空间复杂度为O(1)
(5)算法不稳定。
(6)对于有序序列存在O(n^2n2)的弊端,可以在排序前用random打乱序列顺序。
8.计数排序
8.1算法思路
计数排序核心在于将待排序列数据值转化为键存储在额外开辟的数组空间中。缺陷是待排序列必须有确定的范围,数据是整数,且小规模。
(1)首先扫描整个序列A,获取最大值max;
(2)开辟一块新的空间创建数组B,长度为max+1;
(3)数组B中index的元素记录的值为A中某元素出现的次数;
(4)最后遍历数组B,将数据重写会数组A,即为排好序的序列。
8.2 算法实现
#include<iostream> #include<vector> using namespace std; void countingSort(vector<int>&arr) { int maxNum = arr[0]; for (int i = 1; i < arr.size(); i++) { if (maxNum < arr[i]) maxNum = arr[i]; } int *count = new int[maxNum + 1]; memset(count, 0, sizeof(int)*(maxNum + 1)); for (int i = 0; i < arr.size(); i++) { ++count[arr[i]]; } arr.clear(); for (int i = 0; i <= maxNum; i++) { for (int j = 0; j < count[i]; j++) { arr.push_back(i); } } delete count; } int main() { vector<int>arr = { 6,5,2,6,7,4,3,3,3,1,10 }; countingSort(arr); for (int i = 0; i < arr.size(); i++) { cout << arr[i] << "\t"; } system("pause"); return 0; }
8.3 算法分析
(1)堆排序时间复杂度平均为O(n+k),其中n为数据规模,k为数据最大值。
(2)最坏情况时间复杂度为O(n+k)
(3)最好情况时间复杂度为O(n+k)
(4)空间复杂度为O(n)
(5)算法稳定。