目录
-
认识时间复杂度
常数时间的操作:一个操作如果和数据量没有关系,每次都是固定时间内完成的操作,叫做常数操作。
时间复杂度:时间复杂度为一个算法流程中,在最差情况下,常数操作数量的指标。常用O(读作bigO)来表示。具体来说,在常数操作数量的表达式中,只要高阶项,不要低阶项,也不要高阶项系数,剩下的部分如果记为f(N),那么时间复杂度为O(f(N))。
例如:
- 计算操作次数为(An^2+Bn+C),A为高阶项系数,Bn为低阶,B为低阶系数,时间复杂度我们只要n^2,故时间复杂度为O(n^2)。
- O(1)称为常数时间,具体的值和样本量没有关系。
评价算法的标准:评价一个算法流程的好坏,先看时间复杂度的指标,然后再分析不同数据样本下的实际运行时间,也就是常数项时间。那么O(N)比O(N^2)好。如果两个算法都是O(N),他们的常数操作数量的表达式分别为 1000N+2和10N+10,那么就是10N+10这个算法更好。
一个简单地理解时间复杂度的例子:
一个有序数组A,另一个无序数组B,请打印B中的所有不在A中的数,A数组长度为N,B数组的长度为M。
算法流程1:对于数组B中的每一个数,都在A中通过遍历的方式找一下。这个时间复杂度为O(M*N)
算法流程2:对于数组B中的每一个数,都在A中通过二分的方式找一下。一个数在A中二分查找的时间复杂度为O(logN),即若有N个有序数,我们从中查找到我指定的数最差需要logN次。所以时间复杂度为 M*O(logN)。
算法流程3:先把数组B排序,然后用类似外排的方式打印所有出现在A中出现的数。我们先计B排序用的是快排,时间复杂度为O(MlogM),然后外排的时间复杂度为O(M+N),所以整个算法的时间复杂度为O(MlogM)+O(M+N)。现在我们可以分类讨论了。
- 如果N很小,那么复杂度为O(MlogM) + O(M) = O(MlogM + Mlog2) = O(Mlog2M) = O(MlogM)
- 如果M相比N很小,那么复杂度为O(M+N)
所以这个例子要有具体的样本量才能区分算法流程2和算法流程3哪个好。
- 如果N更小,流程2更好
- 如果M更小,流程3更好
-
排序算法中的稳定性
排序算法稳定性:假定在待排序的记录序列中,存在多个具有相同的关键字的记录,若经过排序,这些记录的相对次序保持不变,即在原序列中,r[i]=r[j],且r[i]在r[j]之前,而在排序后的序列中,r[i]仍在r[j]之前,则称这种排序算法是稳定的;否则称为不稳定的。
举个例子:
对于数组{10,6,8,11,6,5,4,1001,2,1 },对于第一个6和第二个6,他们在经过排序以后,第一个6还是在第二个6的前面,则称这个算法是稳定的。而,如果第一个6和第二个6的在排序后,两个数字的顺序不确定,一会儿第一个6在前,一会第二个6在前,则这个算法为不稳定算法。
-
八大排序性能对比
-
冒泡排序
时间复杂度:O(N^2) ——> N +(N-1) + (N-2)+···+2+1 = AN^2 +BN +C
最好时间复杂度为:O(N^2)
(额外)空间复杂度:O(1)
特点:有限个数即可排完,不需要辅助数组。稳定算法。
应用:工程上不常用,效率不高,适合小数据排序
代码:
#include "pch.h"
#include <iostream>
#include <stack>
#include <vector>
#include<string>
using namespace std;
void swap(int &a, int &b) {
int temp;
temp = a;
a = b;
b = temp;
}
void BubbleSort(int arr[], int n) {
while (n-- > 0) {
for (int i = 0; i < n; i++) {
if (arr[i] > arr[i+1]) {
swap(arr[i],arr[i+1]);
}
}
}
}
int main()
{
int a[10] = { 10,9,8,11,6,5,4,1001,2,1 };
BubbleSort(a, 10);
for (int i = 0; i < 10; i++) {
cout << a[i] << ' ';
}
cout << endl;
return 0;
}
思路分析:
- 首先把数组的最后一个数,排为最大的数,然后将数组的倒数第二个数,排为第二大的数,依次从后往前把数组的每个下标排好。
- 如果一个数比它后一个数大,则将交换这两个数的位置。
-
选择排序
时间复杂度:O(N^2)
最好时间复杂度为:O(N^2)
(额外)空间复杂度:O(1)
特点:有限个数即可排完,不需要辅助数组。不稳定算法。
应用:工程上不常用,数据量大时,它的效率明显高于冒泡,因为选择排序数据移动次数少。
代码:
#include "pch.h"
#include <iostream>
#include <stack>
#include <vector>
#include<string>
using namespace std;
void swap(int &a, int &b) {
int temp;
temp = a;
a = b;
b = temp;
}
void SelectSort(int arr[], int n) {
int minindex;
for (int i = 0; i < (n - 1); i++) {
minindex = i;
for (int j = i + 1; j < n; j++) {
if (arr[minindex] > arr[j]) {
minindex = j;
}
}
swap(arr[i],arr[minindex]);
}
}
int main()
{
int a[10] = { 10,9,8,11,6,5,4,1001,2,1 };
SelectSort(a, 10);
for (int i = 0; i < 10; i++) {
cout << a[i] << ' ';
}
cout << endl;
return 0;
}
思路分析:
- 这个排序和冒泡排序恰好相反,首先把最小的数字找到放在数组第一位上,依次从前往后从小到大地把数组排好。
- 从前往后依次遍历数组中的数,当前数和剩下数组中最小的数相交换,然后继续遍历下一个数。
-
插入排序
时间复杂度:O(N^2)
最好时间复杂度:O(N)
(额外)空间复杂度:O(1)
特点:时间复杂度与数据状况有关。和冒泡排序和选择排序不一样,这两个排序的时间复杂度和数据是否有序无关,而插入排序和数据是否有序是有关系的,如果数据有序则插入排序的时间复杂度为O(N).插入排序是稳定的排序算法。
应用:工程上常用
代码:
#include "pch.h"
#include <iostream>
#include <stack>
#include <vector>
#include<string>
using namespace std;
void swap(int &a, int &b) {
int temp;
temp = a;
a = b;
b = temp;
}
void InsertSort(int arr[], int n) {
for (int i = 1; i < n; i++) {
for (int j = i - 1; (j >= 0 && (arr[j] > arr[j + 1])); j--) {
swap(arr[j], arr[j + 1]);
}
}
}
int main()
{
int a[] = { 1,3,12,10,6,8,11,6,5,4,1001,2,1 };
int n = sizeof(a) / sizeof(a[0]);
InsertSort(a, n);
for (int i = 0; i < n; i++) {
cout << a[i] << ' ';
}
cout << endl;
return 0;
}
思路分析:
- 每步将一个待排序的数字,按其数值的大小插入前面已经排序的数字中的适当位置上,直到全部插入完为止。
- 在数组中,从数组下标为1的位置开始,从前往后遍历需要排序的数字。
- 把关键的代码InsertSort那段短小的代码记下来就OK。
-
归并排序
时间复杂度:O(NlogN)
最好时间复杂度:O(NlogN)
(额外)空间复杂度:O(N)
特点:时间复杂度于数据是否有序的状况无关。是稳定排序算法。它是高级排序算法中,唯一一个稳定的排序算法。
应用:要求排序稳定,空间不重要,则使用归并算法。
代码:
#include "pch.h"
#include <iostream>
#include <stack>
#include <vector>
#include<string>
using namespace std;
void swap(int &a, int &b) {
int temp;
temp = a;
a = b;
b = temp;
}
void merge(int*a, int start, int end, int* result) {
int leftlen = (end - start) / 2 + 1;
int left_index = start;
int right_index = start + leftlen;
int result_index = start;
while (left_index < start + leftlen && right_index < end + 1) {
if (a[left_index] <= a[right_index]) {
result[result_index++] = a[left_index++];
}
else {
result[result_index++] = a[right_index++];
}
}
while (left_index < start + leftlen) {
result[result_index++] = a[left_index++];
}
while (right_index < end + 1) {
result[result_index++] = a[right_index++];
}
}
void merge_sort(int* a, int start, int end, int* result) {
if (end - start == 1) {//只有两个数了
if (a[start] > a[end]) {
swap(a[start], a[end]);
return;
}
}
else if (start == end) {
return;
}
else {
merge_sort(a, start, (end - start) / 2 + start, result); //把左边排好序了
merge_sort(a, (end - start) / 2 + start + 1, end, result); //把右边排好序了
merge(a, start, end, result);
for (int i = start; i < end + 1; i++) {
a[i] = result[i];
}
}
}
int main()
{
int a[] = { 1,3,12,10,6,8,11,6,5,4,1001,2,1 };
const int length = sizeof(a) / sizeof(a[0]);
cout << "数组原始顺序: ";
for (int i = 0; i < length; i++) {
cout << a[i] << " ";
}
cout << endl;
int result[length];
merge_sort(a, 0,length-1,result);
cout << "数组排序后的顺序: ";
for (int i = 0; i < length; i++) {
cout << a[i] << ' ';
}
cout << endl;
return 0;
}
思路分析:以下内容来自百度百科。
归并排序主要分为两部分:
1、划分子区间
2、合并子区间
现在以 9,6,7,22,20,33,16,20 为例讲解上面两个过程:
第一步,划分子区间:每次递归的从中间把数据划分为左区间和右区间。原始区间为[start,end],start=0,end=[length-1],减一是因为数组的下标从0开始,本例中length=8,end=7.现在从中间元素划分,划分之后的左右区间分别为 [start,(end-start+1)/2+start],右区间为[(end-start+1)/2+start+1,end],本例中把start和end带入可以得到[0,7],划分后的左右子区间为[0,4],[5,7],然后分别对[start,end]=[0,4]和[start,end]=[5,7]重复上一步过程,直到每个子区间只有一个或者两个元素。整个分解过程为:
子区间划分好以后,分别对左右子区间进行排序,排好序之后,在递归的把左右子区间进行合并,整个过程如下图所示:
以上代码及思路转自参考链接:http://www.cnblogs.com/rio2607/p/4489893.html
-
快速排序
时间复杂度:O(nlogn)
最好时间复杂度:O(nlogn) ——对应——(额外)空间复杂度:O(logn)
最坏时间复杂度:O(n^2) ——对应——(额外)空间复杂度:O(n) ——最坏情况快排退化为冒泡
平均时间复杂度:O(nlogn)
特点:是不稳定的算法。
应用:当数据集较大时,则应采用时间复杂度为O(nlgn)的排序方法:快速排序、堆排序或归并排序。快速排序是目前基于比较的内部排序中被认为是最好的方法,当待排序的关键字是随机分布时,快速排序的平均时间最短;
代码:
#include "pch.h"
#include <iostream>
using namespace std;
void swap(int &a, int &b) {
int temp;
temp = a;
a = b;
b = temp;
}
void quick_sort(int* a, int start, int end) {
//递归过程先写结束条件
if (start > end) return;
/*if ((end - start) == 1) { //下面的算法包含了这种情况的
if (a[end] < a[start]) {
swap(a[start], a[end]);
}
return;
}*/
//分而治之
int i = start;
int j = end;
int temp = a[start];
while (i != j) {
//总是先动最右边的指针
while ((i < j) && a[j] >= temp) {
j--;
}
while ((i < j) && a[i] <= temp) {
i++;
}
//交换i,j指针指向的值
if (i < j) {
swap(a[i], a[j]);
}
}
//把基准数放入它该在的位置
swap(a[start],a[i]);
//递归基准数左边的数组
quick_sort(a, start, i - 1);
//递归基准数右边的数组
quick_sort(a, i + 1, end);
}
int main()
{
int a[] = { 1,3,12,10,6,8,11,6,5,4,1001,2,1,99,78,79,20,26,33,77 };
const int length = sizeof(a) / sizeof(a[0]);
cout << "数组原始顺序: ";
for (int i = 0; i < length; i++) {
cout << a[i] << " ";
}
cout << endl;
quick_sort(a, 0,length-1);
cout << "排序后的顺序: ";
for (int i = 0; i < length; i++) {
cout << a[i] << ' ';
}
cout << endl;
return 0;
}
思路分析:快速排序从小到大排序:在数组中随机选一个数(默认数组首个元素),数组中小于等于此数的放在左边,大于此数的放在右边,再对数组两边递归调用快速排序,重复这个过程。
-
堆排序
时间复杂度:O(nlogn)
最好时间复杂度:O(nlogn)
最坏时间复杂度:O(nlogn)
平均时间复杂度:O(nlogn)
(额外)空间复杂度:O(1)
特点:是不稳定的算法。
应用:当数据集较大时,所需的辅助空间少于快速排序,并且不会出现快速排序可能出现的最坏情况
代码:
#include "pch.h"
#include <iostream>
using namespace std;
void swap(int &a, int &b);
void heap_sort(int* a, int len);
void BuildMaxHeap(int* a, int len);
void MaxHeap(int* a, int n, int size);
void swap(int &a, int &b) {
int temp;
temp = a;
a = b;
b = temp;
}
void heap_sort(int* a, int len) {
//将数组初始化为大堆
BuildMaxHeap(a, len);
int size = len;
for (int i = size; i > 1; i--) {
swap(a[0], a[i-1]);
size--;
MaxHeap(a,1,size);
}
}
//堆的编号从1~n,对应数组下标0~n-1
void BuildMaxHeap(int* a, int len) {
int n = len / 2;
for (int i = n; i >= 1; i--) {//从倒数第二层的最右结点开始遍历到第一个堆的编号第一的节点。
MaxHeap(a, i, len);
}
}
void MaxHeap(int* a, int n, int size) {
int largestIndex, leftChild, rightChild;
leftChild = n * 2;
rightChild = n * 2 + 1;
if (leftChild <= size && (a[leftChild - 1] > a[n - 1])) {
largestIndex = leftChild;
}
else {
largestIndex = n;
}
if (rightChild <= size && (a[rightChild - 1] > a[largestIndex - 1])) {
largestIndex = rightChild;
}
if (largestIndex != n) {
swap(a[n - 1], a[largestIndex-1]);
MaxHeap(a, largestIndex, size);
}
}
int main()
{
int a[] = { 1,3,12,10,6,8,11,6,5,4,1001,2,1,99,78,79,20,26,33,77 };
int length = sizeof(a) / sizeof(a[0]);
cout << "数组原始顺序: ";
for (int i = 0; i < length; i++) {
cout << a[i] << " ";
}
cout << endl;
heap_sort(a, length);
cout << "排序后的顺序: ";
for (int i = 0; i < length; i++) {
cout << a[i] << ' ';
}
cout << endl;
return 0;
}
思路分析:
- 首先将数组元素建成大小为n的大顶堆,堆顶(数组第一个元素)是所有元素中的最大值
- 然后将堆顶元素和数组最后一个元素进行交换
- 随后,将除了最后一个数的n-1个元素建立成大顶堆,再将最大元素和数组倒数第二个元素进行交换
- 数组下标从后往前排好,按照2,3 步骤重复直至堆大小减为1。
-
希尔排序
时间复杂度:
最好时间复杂度:
最坏时间复杂度:
平均时间复杂度:
(额外)空间复杂度:
特点:是不稳定的算法。
应用:
代码: