1. 时间复杂度分析

一. 对数据规模又一个概念

想要在1s内解决问题:

  • O(n2)的算法可以处理大约104级别的数据
  • O(n)的算法可以处理大约10^8级别的数据
  • O(nlogn)的算法可以处理大约10^7级别的数据
  • 保险起见,在实际中最好降一个级

空间复杂度

  • 递归调用是有空间代价的
空间复杂度O(1):

int sum1(int n){
    assert(m >= 0);
    int ret = 0;
    for(int i = 0; i <= n; i++)
        ret += i;
    return ret;
}



空间复杂度O(n):

int sum2(int n ){
    assert(n >= 0);
    if( n == 0)
        return 0;
    return n + sum2(n-1);
}

二. 简单的复杂度分析

  • O(n2)O(n2)

选择排序
void selectionSort(int arr[], int n){
    for(int i=0; i < n; i++){
        int minIndex = i;
        for(int j=i+1; j < n; j++){
            minIndex = j;
        }
        swap( arr[i], arr[minIndex]);
    }
}

  • O(n)O(n)

下面的代码是 30n次操作, 是O(n)级别的, 容易被当成O(n^2)
void printInformation(int n) {
    for( int i = 1; i <= n; i++)
        for( int j = 1; j <= 30; j++){
            cout<<"class"<<i<<" - "<<"No. "<<j<<endl;
        }
    return;
}

  • O(logn)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;
}


  • O(logn)O(logn)

正整数转化为字符
string intToString( int num ){
    string s = "";
    while(num) {
        s += '0' + num%10;
        num /= 10;
    }
    
    reverse(s);
    return s;
}

分析: n经过几次"除以10"操作后, 等于0?

log10n=O(logn)log10​n=O(logn)


为什么 log10nlog10​n 和 log2nlog2​n 都是O(logn)O(logn)级别的?

答:

logaNloga​N 和 logbNlogb​N可以相互转换:

logaN=logab∗logbNloga​N=loga​b∗logb​N

logabloga​b是一个常数


  • O(nlogn)O(nlogn)

void hello(int n){
    for (int sz = 1; sz < n; sz += sz)   // logn
        for( int i = 1; i < n; i++){       //n
            cout<<"hello , Algorithm!"<<endl;
        }
}

第一个forlog2nlog2​n 第二个for是nn, 结合起来就是O(nlogn)O(nlogn)


  • O(n)O(n​)

判断n是否为素数
bool isPrime( int n){
    for(int x=2; x*x <= n; x++)
        if( n%x == 0)
            return false;
    return true;
}

假如n不是素数, 必然有q<sqrt(n)和p>sqrt(n), p*q=n。
所以不难理解为什么x*x<=n了。

该程序还可以优化, 利用素数都是满足6x-1或6x+5的特性。 具体实现google一下即可

三. 递归算法的复杂度分析

递归中进行一次递归调用的复杂度分析

  • 如果递归函数中, 只进行一次递归调用
  • 递归深度为depth
  • 在每个递归函数中, 时间复杂度为T
  • 则总体的时间复杂度为O(T*depth)
  • 实际案列
下面代码每次递归都少一半,所以depth是O(logn)  
T显然是1
总体就是O(logn)


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)
}

下面代码每次递归都减少1 ,所以depth为n
T显然是1
总体就是 O(n)

int sum(int n){
    assert( n >= 0);
    if( n== 0)
        return 0;
    return n + sum(n-1);
}

下面求pow的代码: depth=logn    T=1   总体O(logn)
用递归的方法比用n个x相乘,for循环实现的算法O(logn), 要快得多


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 ;  // n如果是奇数,n/2会舍掉一个1, 要补上
    
    return t*t;
}

递归中进行多次递归调用

  • 实例一O(2n)O(2n)
int f(int n){
    assert( n >= 0);
    if( n == 0)
        return 1;
    return f(n-1) + f(n-1)
}

该程序的递归调用过程如下
                    3
                /      \
              2          2
             / \        / \
            1   1      1   1
           / \ / \    / \ / \
          0  0 0  0  0  0 0  0
    
两次的递归调用, 过程看作是二叉树      
每一层处理的数字量是-1的, 一共n+1层
一共进行
2^0 + 2^1 + 2^2 + ... + 2^n
=2^(n+1) - 1 次运算, 
每层运算复杂度是常数, 所以
O(2^n)
  • 实例二O(nlogn)O(nlogn)
归并排序:
void mergeSort(int arr[], int l, int r){
    if(l >= r)
        return;
    
    int mid = (l+r)/2;
    mergeSort(arr, l, mid);
    mergeSort(arr, mid+1, r);
    merge(arr, l, mid, r);
}

该程序的递归调用过程如下
                    8
                /      \
              4          4
             / \        / \
            2   2      2   2
           / \ / \    / \ / \
          1  1 1  1  1  1 1  1
          
两次的递归调用, 过程看作是二叉树
每层处理的数字两是减半的, 一共4层
进行了2^0 + 2^1 + .... + 2^log2(n)  次运算

每次运算处理的量为n,
所以O(nlogn)


四. 均摊复杂度分析

  • 下面是一个动态数组的代码
#ifndef INC_06_AMORTIZED_TIME_MYVECTOR_H
#define INC_06_AMORTIZED_TIME_MYVECTOR_H

template <typename T>
class MyVector{

private:
    T* data;
    int size;       // 存储数组中的元素个数
    int capacity;   // 存储数组中可以容纳的最大的元素个数

    // 复杂度为 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;
        capacity = newCapacity;
    }

public:
    MyVector(){

        data = new T[100];
        size = 0;
        capacity = 100;
    }

    ~MyVector(){

        delete[] data;
    }

    // 平均复杂度为 O(1)
    void push_back(T e){

        if(size == capacity)
            resize(2 * capacity);

        data[size++] = e;
    }

    // 平均复杂度为 O(1)
    T pop_back(){

        assert(size > 0);
        size --;

        return data[size];
    }

};

#endif //INC_06_AMORTIZED_TIME_MYVECTOR_H

  • 分析
一开始容量capacity=100, 这时候往MyVector加元素时, 时间复杂度是O(1)
当size=capacity时, 我们需要resize,在for中进行了capacity次操作,复杂度为o(n)。
但这中操作只有当size=capacity时,才会做。在这之前进行了capacity次添加元素push_back, 每次复杂度为1。
将resize的操作均摊到之前的push_back上, 每次push_back由1变为了2, 算法依然是O(1)

五. 避免复杂度的震荡

  • 假如MyVector中存储了大量元素,突然要删除部分元素, 我们在pop_back的时候并没有将多余的空间缩小。
  • 完善pop_back
    // 平均复杂度为 O(1)
    T pop_back(){

        assert(size > 0);
        T ret = data[size-1];
        size --;

    
        // resize的容量是当前最大容量的1/2
        if(size == capacity / 2)
            resize(capacity / 2);

        return ret;
  • 对现在的pop_back进行均摊分析
假设当前容量capacity为2n, 当pop_back到size=n时, 会resize,复杂度为O(n)
与之前的pop_back均摊以后, pop_back由1变为2, 复杂度依然为O(1)

但是将push_back和pop_back一起看时, 会发现问题

  • 当非常不巧的, 我们在size=capacity的地方,先push_back再pop_back,push_back和pop_back交替进行
  • 这样每次push_back或pop_back都要resize,复杂度变为了O(n)
  • 这样就与均摊的复杂度产生矛盾了。形成了复杂度震荡

解决办法

  • 让push_back和pop_back错开
  • push_back的resize判断条件依然不变
  • pop_back的resize判断条件变为 size=capacity/4
    // 平均复杂度为 O(1)
    T pop_back(){

        assert(size > 0);
        T ret = data[size-1];
        size --;

        // 在size达到静态数组最大容量的1/4时才进行resize
        // resize的容量是当前最大容量的1/2
        // 防止复杂度的震荡
        if(size == capacity / 4)
            resize(capacity / 2);

        return ret;

猜你喜欢

转载自blog.csdn.net/weixin_41207499/article/details/84247593
今日推荐