一、预备知识(基本解题思路与复杂度分析)

一、预备知识(基本解题思路与复杂度分析)

算法面试可以看做是和面试官探讨解决方案,对于问题的细节和应用环境可以和面试官沟通
沟通本身很重要,暗示了思考问题的方式

算法学习准备范围

  • 各种排序算法
  • 基础数据结构和算法实现:堆、二叉树、堆…

算法题基本解题思路

1. 注意条件

  • 题目中的一些条件是对问题的限制 如给定一个有序数组
  • 也有一部分条件是对题解的暗示 如 设计一个O(nlogn)的算法

2. 没有思路时

  • 试验几个简单的测试用例
  • 不忽视暴力解法,避免面试过程中出现没有想法的局面

3. 优化算法

  • 遍历常见的算法思路
  • 遍历常见的数据结构
  • 考虑时间与空间的交换(哈希表)
  • 预处理信息(排序)
  • 在瓶颈处寻找答案:O(nlogn)+O(n2);O(n3)

4. 编写程序

  • 首先注意极端条件的判断
    数组为空?字符串为空?数量为0?指针为NULL?
  • 变量名要规范,变量名的含义一定要明确
  • 注意模块化,复用性

5. 针对基本问题,使用白板编程

时间复杂度分析

时间复杂度

O(f(n))
O(1)、O(logn)、O(n)、O(n2)、O(cn)
学术界:O(f(n))表示算法执行上界
业界:O(f(n))经常表示算法执行的最低上界

一个时间复杂度的问题

有一个字符串数组,将数组中的每一个字符串按照字母排序;之后再将整个字符串数组按照字典序排序。整个操作的时间复杂度?

假设最长的字符串长度为s,数组中有n个字符串:
将数组中的每一个字符串按照字母排序O(n*slogs);
将整个字符串数组按照字典序排序O(s**nlogn)(此处乘以s是因为当两个字符串比较时,复杂度就应该乘以s,平常的nlogn是因为整数的比较复杂度为O(1))

此处应该注意描述算法复杂度时,使用到了两个量s和n

算法复杂度在某些情况下是与用例相关的
对数据规模有一个概念

以下为一个测试用例

#include 
#include 
#include 
using namespace std;
int main(){
    for(int i=1; i<=9; ++i){
        int n = pow(10, i);
        
        clock_t start_time = clock();
        int sum=0;
        for(int j=0; j<n; ++j){
            sum += j;
        }
        clock_t end_time = clock();
        
        cout << "10^" << i << ":" << double(end_time-start_time)/CLOCKS_PER_SEC << " s" << endl;
    }
    
    return 0;
}

我的vscode跑出来的结果如下

10^1:0 s
10^2:0 s
10^3:0 s
10^4:0 s
10^5:0 s
10^6:0.004 s
10^7:0.035 s
10^8:0.348 s
10^9:3.463 s
如果想要在1s内解决问题
  • O(n2)的算法可以处理约104级别的数据
  • O(n)的算法可以处理约10^8级别的数据
  • O(nlogn)的算法可以处理约10^7级别的数据

空间复杂度

多开一个辅助数组:O(n)
多开一个辅助的二维数组:O(n^2)
多开常数空间:O(1)–在一个数组原地进行操作

递归调用是有代价空间的
比如一个累加的小程序

空间复杂度为O(1)

int sum1(int n){
    assert(n>=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 sum(n-1) + n;
}

常见的复杂度分析

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){
        min_index = i;
        for(int j=i+1; j<n; ++j){
            if(arr[j] < arr[min_index]
                min_index = j;
        }
        
        swap(arr[i], arr[min_index]);
    }
}

O(n^2)??? × --》O(n)

void printInformation(int n){
    for(int i=1; i<=n; ++i){
        for(int j=0; j<=30; ++j){
            cout << "Class" << i << " - " << "N0. " << j << endl;
        }
    }
}

O(nlogn) 二分查找 n经过几次“除以2”的操作后,等于1

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)

string intToString(int num){
    string s = "";
    
    while(num){
        s += '0'+num%10;
        num /=10;
    }
    
    reverse(s);
    return s;
}

O(n^2)??? × --》O(nlogn) 注意循环起始点 终止点 以及变量是如何变化的

void hello(int n){
    for(int sz=0; sz<n; sz += sz){
        for(int i=0; i<n; ++i){
            cout << "hello algorithm" << end;
        }
    }
}

O(sqrt(n))

bool isPrime(int n){
    for(int x=2; x*x<=n; ++x){
        if(n%x==0)
            return false;
    }
    return true;
}

复杂度实验

本节主要说明在不同的数据规模在各个复杂度的算法中的运行时间比较
不做详细说明,见网课2-4节

递归算法的复杂度分析

不是有递归的函数就一定是O(nlogn)!!!

递归中进行一次递归调用的复杂度分析
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;
    if(arr[mid]>target){
        return binarySearh(arr, l, mid-1, target)
    }else{
        return binarySearch(arr, mid+1, r, target)
    }
}

该算法复杂度为O(logn),因为每一次递归中的操作的时间复杂度为O(1),递归深度为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)

递归中进行多次递归调用

注意调用的次数
比如

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

调用次数可以见递归树,总共调用了20+21+22+…+2n=2^(n+1)-1, 因此算法复杂度为O(2^n)

void mergeSort(int arr[], int l, int r){
    if(l>=r)
        return ;
    int mid = l+(r-l)/2;
    mergeSort(arr, l, mid);
    mergeSort(arr, mid+1, r);
    merge(arr, l, mid, r);
}

递归树,一共有logn层,每一层处理的数据规模为n,每个节点的是O(n)级别, 因此时间复杂度为O(nlogn)

递归的算法复杂度研究,可以学习主定理

均摊复杂度分析

当有一个算法复杂度相对较高,但是该算法复杂度高是为了方便其他的复杂度,因此有时需要将该算法与其他复杂度一起计算
最典型的例子就是动态数组中删除,添加元素


/***********
 均摊复杂度分析使用的动态数组
 ************/
#include 
using namespace std;
template <typename T>
class myVector{
private:
    T* data;
    int capacity; // 数组的最大容量
    int size;  // 当前数组的大小
    void resize(int new_capacity){
        T* new_data = new T[new_capacity];
        for(int i=0; i<size; ++i){
            new_data[i] = data[i];
        }
        delete[] data;
        data = new_data;
        capacity = new_capacity;
    }
public:
    myVector(){
        data = new T[10];
        capacity = 10;
        size=0;
    }
    ~myVector(){
        delete[] data;
    }
    // 均摊复杂度分析
    // 只在特定点出算法复杂度为O(n),但是在其余位置均为O(1)
    // 平均2个操作
    // 均摊复杂度为O(1)
    void push_back(T e){
        
        if(size==capacity)
            resize(2*capacity);
        data[size++] = e;
    }
    // 均摊复杂度为O(1)
    T pop_back(){
        T ret = data[size-1];
        size--;
        if(size==capacity/4)
            resize(capacity/2)  // 避免震荡,总在节点处进行删除、增加元素的操作
        
        return ret;
        
    }
    
}

你可能感兴趣的:(liu算法刷题学习,算法,数据结构,面试,leetcode)