算法面试可以看做是和面试官探讨解决方案,对于问题的细节和应用环境可以和面试官沟通
沟通本身很重要,暗示了思考问题的方式
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
多开一个辅助数组: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;
}
}