从基本排序算法看时间复杂度
1.什么是时间复杂度
在介绍时间复杂度分析方法前,我们首先来明确下算法的运行时间究竟取决于什么。直观地想,一个算法的运行时间也就是执行所有程序语句的耗时总和。然而在实际的分析中,我们并不需要考虑所有程序语句的运行时间,我们应该做的是集中注意力于最耗时的部分,也就是执行频率最高而且最耗时的操作。也就是说,在对一个程序的时间复杂度进行分析前,我们要先确定这个程序中哪些语句的执行占用的它的大部分执行时间,而那些尽管耗时大但只执行常数次(和问题规模无关)的操作我们可以忽略。我们选出一个最耗时的操作,通过计算这些操作的执行次数来估计算法的时间复杂度,下面我们来具体介绍这一过程。
int count(int a[],int N)
{
int cnt = 0;
for (int i = 0; i < N; i++) {
for (int j = i + 1; j < N; j++) {
for (int k = j + 1; k < N; k++) {
if (a[i] + a[j] + a[k] == 0) {
cnt++;
}
}
}
}
return cnt;
}
首先我们看到以上代码的第1行的语句只会执行一次,因此我们可以忽略它。然后我们看到第2行到第10行是一个三层循环,最内存的循环体包含了一个if语句。也就是说,这个if语句是以上代码中耗时最多的语句,我们接下来只需要计算if语句的执行次数即可估计出这个算法的时间复杂度。以上算法中,我们的问题规模为N(输入数组包含的元素数目),我们也可以看到,if语句的执行次数与N是相关的。我们不难得出,if语句会执行N * (N - 1) * (N - 2) / 6次,因此这个算法的时间复杂度为O(n^3)。由此我们也可以知道,算法的时间复杂度刻画的是随着问题规模的增长,算法的运行时间的增长速度是怎样的。在平常的使用中,Big O notation通常都不是严格表示最坏情况下算法的运行时间上限,而是用来表示通常情况下算法的渐进性能的上限,在使用Big O notation描述算法最坏情况下运行时间的上限时,我们通常加上限定词“最坏情况“。
通过以上分析,我们知道分析算法的时间复杂度只需要两步(比把大象放进冰箱还少一步:) ):
- 寻找执行次数多的语句作为决定运行时间的[关键操作];
- 分析关键操作的执行次数。
2.快速判断法则:
1.for循环
一个for循环的运行时间至多是该for循环内部那些语句(包括测试)的总运行时间乘以迭代的次数
2.嵌套的for循环
从里向外,一组嵌套循环内部的一条语句总的运行时间为该语句的运行时间乘以该组所有的for循环的大小的乘积
3.顺序语句
各语句运行时间求和
4.if/else语句
一个if/else语的运行时间不超过 判断的运行时间+if语句/else语句中较长的运行时间
3.基本算法中看时间复杂度
/*
插入排序的核心思想在于寻找一个合适的位置,并把相应的元素插入进去。
特点:1.时间复杂度与插入顺序有关,当数组有序(接近有序)算法复杂度将远远低于随机数组
2.比较次数 = 交换次数 + 额外项(此项为N减去已知最小元素次数,如最坏情况倒序数组为N - 1)
算法复杂度:
平均情况需要 N^2/4 比较和交换,最坏则需要N^2/2 次交换和比较,最好情况是N -1 次比较不需要交换。
时间复杂度:最佳O(N)其他情况为O(N^2)
空间复杂度: O(1)
*/
#include <iostream>
#include "basic_algo.h"
using std::cout;
using std::cin;
using std::endl;
void insert(int list[], int length)
{
for (int i = 1; i < length; i++)
{
for (int j = i; j > 0 && less<int>()(list[j],list[j-1]); j--)
{
exch<int>()(list, j, j - 1);
}
}
}
int main()
{
int a[5];
for (int i = 0; i < 5; i++)
cin >> a[i];
insert(a, 5);
show<int>()(a, 5);
return 0;
}
/*
归并排序的核心思想类似与分治策略,通过不断分治,将数组不断缩小,直到1, 合并生成2的有序数组,在
继续向上生成合并,直到生成有序数组;
本程序采用和自顶向下的方法,不断产生子数组,依靠子数组的有序,进行合并。并且将合并的实现提取出来
防止由于大数组导致递归的失败的问题;
特点:1.采用分而治之的思想,将问题规模不断缩小。
2.在空间复杂度上,与N成正比。
算法复杂度
1.时间复杂度为O(n log(n)),在merge中会产生一个递归树,由树的性质可知第K层,一共有2^k的
叶子节点,并且每个叶子节点的长度为2^(n -k)所以每层需要比较2^n,又因为树高为n,所以一共
需要比较2^n*n次 = nlgn;
2.空间复杂度为数组的长度O(N)
*/
#include <iostream>
#include "basic_algo.h"
using std::cout;
using std::cin;
using std::endl;
void merge_aux(int list[], int lo, int middle, int hi)
{
int a[hi - lo];
for (int k = lo; k <= hi; k++)
{
a[k] = list[k];
}
int i = lo;
int j = middle + 1;
for (int k = lo; k <= hi; k++)
{
if (i > middle)
list[k] = a[j++];
else if (j > hi)
list[k] = a[i++];
else if (less<int>()(a[j], a[i]))
list[k] = a[j++];
else
list[k] = a[i++];
}
}
void merge(int list[], int lo, int hi)
{
if (hi <= lo)
return;
int mid = (lo + hi) / 2;
merge(list, lo, mid);
merge(list, mid + 1, hi);
merge_aux(list, lo, mid, hi);
}
int main()
{
int a[6];
for (int i = 0; i < 6; i++)
cin >> a[i];
merge(a, 0, 5);
show<int>()(a, 6);
return 0;
}
/*
希尔排序的核心思想是不断生成间隔h的有序数组,本算法使用的是递增性数组
生成递增数组用的是直插排序的思想,寻找元素适合的位置。
特点:1.权衡了子数组的规模和有序性,使得插入排序排序的数组接近有序,从而使插入排序的性能得到体现
2.希尔排序适合对大数组的排序,并且数组越大,优势越大
算法复杂度
1.希尔排序的时间复杂度为O(n^3/2),希尔排序时间复杂度的下界是n*log2n\
2.空间复杂度为O(1)
*/
#include <iostream>
#include "basic_algo.h"
using std::cout;
using std::cin;
using std::endl;
void shell(int list[], int length)
{
int h = 1;
while (h < length / 3)
{
h = h * 3 + 1;
}
while (h >= 1)
{
for (int i = h; i < length; i++)
{
for (int j = i; j >= h && less<int>()(list[j], list[j-h]);j -= h)
{
exch<int>()(list, j, j - h);
}
}
h = h / 3;
}
}
int main()
{
int a[5];
for (int i = 0; i < 5; i++)
cin >> a[i];
shell(a, 5);
show<int>()(a, 5);
return 0;
}
/*
快速排序的核心思想类似与归并排序,都是采用分治策略,但是归并是有序子序列的生成有序序列,而快速排序
采用切分,将数组不断进行切分,切分的数组生成有序的序列的时,整个序列也就有序了。切分的思想在于从
数组两端不断迭代,找到一个监视站正好可以将其分成两个数组的部分并交换。
特点:1.时间复杂度和空间复杂度都较低,是一个性能良好的算法,可以继续优化(三切分等)!
2.有一个很大缺点,就是切分不均衡问题,解决方法随机打乱排列!
算法复杂度:
1.时间复杂度:
假定Cn为n个元素所需要排序的比较次数
Cn = 1 + n + (Cn-1 + Cn-2 + .... + C0) * 2 / n; 前面为切分的成本,后面为左右子序列成本;
nCn = n(n + 1) + 2 (C0+ ... + Cn - 1);0,
同时减去n - 1
nCn - (n - 1)Cn-1 = 2n + 2Cn - 1
同时除以n + 1
Cn/(n + 1) = Cn-1/ n + 2 / (n + 1)
所以 Cn ~ 2(n + 1)(1 / 3+ 1 / 4 + ....+ 1 / (n+1))
Cn ~ 2nln(n) ~ 1.39nlgn
2.空间复杂度 o(lgn) ~ o(n) //最乐观情况折半查找的时间复杂度。最不乐观情况切分n - 1
*/
#include <iostream>
#include "basic_algo.h"
using std::cout;
using std::cin;
using std::endl;
int partition(int list[], int lo, int hi)
{
int i = lo;
int j = hi + 1;
int v = list[i];
while (1)
{
while (less<int>()(list[++i], v))
if (i == hi)
break;
while (less<int>()(v,list[--j]))
if (j == lo)
break;
if (i >= j)
break;
exch<int>()(list, i, j);
}
exch<int>()(list, lo, j);
return j;
}
int partition2(int list[], int lo, int hi) //nomoral aux
{
int i = lo;
int j = hi + 1;
int v = list[i];
while(i < j)
{
while (i < j && less<int>()(v,list[j]))
{
--j;
}
list[i] = list[j];
while (i < j && less<int>()(list[i], v))
{
++i;
}
list[j] = list[i];
}
list[i] = v;
return i;
}
void quick(int list[], int lo, int hi)
{
if (hi < lo)
return;
int j = partition2(list, lo, hi);
quick(list, j+1, hi);
quick(list, lo, j-1);
}
int main()
{
int a[6];
for (int i = 0; i < 6; i++)
cin >> a[i];
quick(a, 0, 5);
show<int>()(a, 6);
return 0;
}
/*
堆排序是利用堆这种数据结构而设计的一种排序算法,堆排序是一种选择排序。
堆是具有以下性质的完全二叉树:每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆或者每个结点
的值都小于或等于其左右孩子结点的值,称为小顶堆。而堆排序是基于前者大顶堆
特点:1.它的最坏,最好,平均时间复杂度均为O(nlogn),它也是不稳定排序
算法复杂度:
1.时间复杂度为O(nlogn)
2.空间复杂度 O(1)
ps:原谅我这个证明 实在是看不懂吐血,但是本着基本算法还是写上来了。
*/
#include <iostream>
#include "basic_algo.h"
using std::cout;
using std::cin;
using std::endl;
/*
二叉树的性质:第N个节点的孩子节点为 2 * N + 1 OR 2 * N + 2
*/
void sink(int list[], int length, int k)
{
while(2 * k <= length)
{
int j = 2 * k;
if (j < length && less<int>()(list[j], list[j+1]))
j++;
if(!less<int>()(list[k], list[j]))
break;
exch<int>()(list,k, j);
k = j;
}
}
void heapsort(int list[], int length)
{
for(int k = length / 2; k >= 1; k--)
{
sink(list, length, k);
}
while (length >= 1)
{
exch<int>()(list, 1, length--);
sink(list, length, 1);
}
}
int main()
{
int a[6];
for (int i = 1; i < 6; i++)
cin >> a[i];
heapsort(a, 5);
show<int>()(a + 1, 5);
return 0;
}
ps:这些代码中用到的仿函数在库中均有实现,只不过我自己又实现了一遍。
这里是全部的代码,需要用的可以自取github链接