到底什么是大O
n表示数据规模。
O(f(n))表示运行算法所需要执行的指令数,和f(n)成正比。
算法A: O(n) 所需的执行指令数:10000 n
算法B: O(n^2) 所需执行的指令数:10*n ^2
随着数据规模n的增大,两个算法的执行时间为:
不同算法复杂度的比较
学术界:O(f(n))表示算法执行的上界。
工业界:O表示算法执行的最低上界。
一个字符串数组,将数组中的每一个字符串按照字母序排序;之后再将整个字符串数组按照字典序排序。整个操作的时间复杂度?
假设最长的字符串长度为s;数组中有n个字符串
对每个字符串排序:O(slogs)
将数组中的每一个字符串按照字母序排序:O(n slog(s))
将整个字符串数组按照字典序排序:O(s nlog(n))
O(n slog(s))+O(s nlog(n))=O(nslogs+snlogn)
=O(ns(logs+logn))
对字符串数组进行字典序排序时,进行nlogn次,每次比较需要s次
复杂度分析和用例相关
数据规模的概念
如果要在1s解决问题:
空间复杂度
递归调用是有空间代价的,递归的空间复杂度等于递归的深度
常见代码的复杂度
- O(1)
//两数交换
void swapTwoInts(int &a, int &b){
int temp = a;
a = b;
b = temp;
}
- O(n)
//求和
int sum (int n){
int ret = 0;
for(int i = 0; i <= n; i++)
ret += i;
return ret;
}
//翻转字符串
void reverse(string & s){
int n = s.size();
for(int i = 0; i < n/2; i++)
swap(s[i], s[n - 1 - i]);
}
- O(n^2)
//选择排序
void selectionSort(int arr[], int n){
for(int i = 0; i < n; i++){
int minIndex = i;
for(int j = i + 1; j < n; j++){
if(arr[j] < arr[minIndex])
minIndex = j;
}
swap(arr[i], arr[minIndex]);
}
}
- O(logn)
//二分查找
int binarySearch(int arr[], int n, int target){
int l = 0, r = n -1 ;
while(l <= r){
int mid = l + (r - l) / 2;
if(arr[mid] == target)
return mid;
if(arr[mid] > target)
r = mid - 1;
else
l = mid + 1;
}
return -1;
}
//将数字整型转换为字符串
string intTostring(int num ){
string s ="";
while(num){
s += '0' + num % 10;
num /= 10;
}
reverse(s);
return s;
}
void hello (int n){
for(int sz = 1; sz < n; sz += sz)
for(int i = 1; i < n; i++)
cout << "hello! " << endl;
}
- O(sqrt(n))
//判断是否是素数
bool isPrime(int n){
for(int x = 2; x * x <= n; x < n)
if(n % x == 0)
return false;
return true;
}
复杂度试验
如果想要在1s内解决问题:
实验,观察趋势
每次将数据规模提高两倍,看时间的变化
递归算法的复杂度
1. 递归中进行一次递归调用的复杂度分析
int binarySearch(int arr[], int l, int r, int target){
if(l > r)
return -1;
int mid = l + (r - l)/ 2;
if(arr[mid] == target)
return mid;
else if(arr[mid] > target)
return binarySearch(arr, l, mid - 1, target);
else
return binarySearch(arr, mid +1; r, target);
}
递归深度为 O(logn), 函数处理问题复杂度为O(1),所以时间复杂度为 O(logn)
如果递归函数中,只进行一次递归调用,递归深度为depth,在每个递归函数中,时间复杂度为T;
则总体的时间复杂度为O(T*depth)
例一:
int sum(int n){
assert(n >= 0);
if(n == 0)
return 0;
return n + sum(n - 1);
}
递归深度为n;
时间复杂度为O(n);
例二:
double pow(double x, int n){
assert(n >= 0);
if(n == 0)
return 1.0;
double t = pow(x, n / 2);
if( n % 2)
return x*t*t;
return t * t;
}
递归深度为logn;
时间复杂度为O(logn);
2 递归中进行多次递归调用
int f (int n){
assert(n >= 0);
if( n == 0)
return 1;
return f(n - 1) + f(n -1 );
}
计算调用的次数
调用次数:1+2+4+8=15;
调用次数:O(2^n)
递归函数复杂度还可以通过主定理分析
均摊复杂度分析
动态数组(vector)
#include <iostream>
#include<assert.h>
using namespace std;
template <typename T>
class myVector{
private:
T * data;
int capacity; //存储数组中可以容纳的最大元素的个数
int size; //存储数组中元素的个数
//O(n)
void resize(int newCapacity){
assert(newCapacity >= size);
T *newData = new T[newCapacity];
for (int i = 0; i < size; i++){
newData[i] = data[i];
}
delete[] data;
//data指针指向新开辟的newData
data = newData;
capacity = newCapacity;
}
public:
myVector(){
data = new T[10];
capacity = 10;
size = 0;
}
//析构函数
~myVector(){
delete[] data;
}
//平均O(1)
void push_back(T e){
//assert(size < capacity);
if (size == capacity)
resize(2 * capacity);
data[size++] = e;
}
T pop_back(){
assert(size > 0);
T ret = data[size - 1];
size--;
//防止复杂度震荡
if (size == capacity / 4)
resize(capacity / 2);
return ret;
}
};
#include <cmath>
#include <ctime>
#include "myVector.h"
using namespace std;
int main(){
for (int i = 10; i <= 26; i++){
int n = pow(2, i);
clock_t startTime = clock();
myVector<int> vec;
for (int i = 0; i < n; i++){
vec.push_back(i);
}
for (int i = 0; i < n; i++)
vec.pop_back();
clock_t endTime = clock();
cout << 2 * n << " operations : \t";
cout << double(endTime - startTime) / CLOCKS_PER_SEC << " s" << endl;
}
system("pause");
return 0;
}
假设当前数组容量为n,
添加时,当数组容量小于n时,时间复杂度为O(1),当容量为n时,调用resize函数,时间复杂度为O(n),前n次添加的时间复杂度为O(1), 一共n次操作,加上最后一次的n,一共需要2n次操作,均摊后,每次操作为O(2),所以动态数组的添加操作时间复杂度为O(1).
实验