[MIT算法导论]笔记全集

(该文章只记载除数据结构以外的算法课内容,数据结构部分请查看其它文章)

目录


算法时间复杂度

主方法

  • case 1
    f ( n ) = O ( n l o g b a − ε ) ) ( ε > 0 ) = > T ( n ) = Θ ( n l o g b a ) f(n)=O(n^{ {log}_ba-ε})) (ε>0) =>T(n)=Θ(n^{ {log}_ba}) f(n)=O(nlogbaε))ε>0=>T(n)=Θ(nlogba)
  • case 2
    f ( n ) = Θ ( n l o g b a ⋅ ( l g n ) k ) ( k ≥ 0 ) = > T ( n ) = Θ ( n l o g b a ⋅ ( l g n ) k + 1 ) f(n)=Θ (n^{ {log}_ba}·(lgn)^k)(k≥0) =>T(n)=Θ(n^{log_ba}·(lgn)^{k+1}) f(n)=Θ(nlogba(lgn)k)k0=>T(n)=Θ(nlogba(lgn)k+1)
  • case 3
    f ( n ) = Ω ( n l o g b a + ε ) ) ( ε > 0 ) f(n)=Ω(n^{ {log}_ba+ε}))(ε>0) f(n)=Ω(nlogba+ε))ε>0&& 存在 0 < ε ′ < 1 0<ε'<1 0<ε<1,使得 a f ( n / b ) < = ( 1 − ε ’ ) ⋅ f ( n ) af(n/b)<=(1-ε’)·f(n) af(n/b)<=(1ε)f(n)
    => T ( n ) = Θ ( f ( n ) ) T(n)=Θ(f(n)) T(n)=Θ(f(n))

case 1可用递归树证明:
在这里插入图片描述

在这里插入图片描述


分治法(Divide and Conquer)

归并排序

排序步骤:

  • n为1时,已经完成排序。

  • n大于1时,递归的对 A [ 1.. ⌈ n / 2 ⌉ ] A[1 ..⌈n/2⌉] A[1..n/2]这部分以及 A [ ⌈ n / 2 ⌉ + 1.. n ] A[⌈n/2⌉+1..n] A[n/2+1..n]这部分排序。

  • 将排序好的两个表合并,比较两个表,依次取出最小者到输出序列中。

  • 运行时间:
    递归函数为 T ( n ) = 2 T ( n / 2 ) + Θ ( n ) T(n)=2T(n/2)+Θ(n) T(n)=2T(n/2)+Θ(n)。(递归时间+非递归时间)
    使用主方法,为case2 ,所以 T ( n ) = Θ ( n l g n ) T(n)= Θ(nlgn) T(n)=Θ(nlgn)

乘方问题

和二分查找实质一样,将指数n二分:

有实数 X X X和正整数 n n n计算 X n X^n Xn
当n为偶数时, X n X^n Xn= X n / 2 X^{n/2} Xn/2· X n / 2 X^{n/2} Xn/2,当n为奇数时, X n X^n Xn= X ( n − 1 ) / 2 X^{(n-1)/2} X(n1)/2· X ( n − 1 ) / 2 X^{(n-1)/2} X(n1)/2· X X X

  • 运行时间:
    递归函数为 T ( n ) = T ( n / 2 ) + Θ ( 1 ) T(n)=T(n/2)+Θ(1) T(n)=T(n/2)+Θ(1)
    使用递归树法 T ( n ) = Θ ( l g n ) T(n)= Θ(lgn) T(n)=Θ(lgn)

斐波那契数列

  1. F n = φ n / √ 5 Fn=φ^n/√5 Fn=φn/5结果取最接近的整数(round)。(φ是黄金分割数)
    该方法无法用计算机实现(精确度不够的原因)。
    运行时间: Θ ( l g n ) Θ(lgn) Θ(lgn)

  2. 平方递归式:

(F(n+1)  Fn)  = (1 1)n
(Fn  F(n-1))    (1 0)

证明方法可采用归纳法:

  1. (F2 F1) = (1 1)
    (F1 F0) (1 0)
  2. (F(n+1) Fn) = (Fn F(n-1)) (1 1)
    (Fn F(n-1)) (F(n-1) F(n-2)) (1 0)
    由定义可知上式成立。

快速排序

运行时间:

  • 最坏情况:
    输入数据本身已经排序或逆排序,划分的一侧始终无元素。
T(n)=T(0)+T(n-1)(n)
=Θ(1)+T(n-1)(n)
=T(n-1)(n)
=Θ(n^2)(递归树法)
  • 最优情况:
    每次的关键数据处于正中间 划分为两个长度相等的部分
T(n)=2T(n/2)(n)=Θ(nlgn)
  • 特殊情况:
    每次的关键数据处于特殊位置。比如:划分为两个长度分别占1/10 9/10:
    在这里插入图片描述

  • 交替产生特殊情况和最坏情况:

L(n)=2U(n/2)(n)(lucky)
U(n)=L(n-1)+ Θ(n)(unlucky)
Then L(n)=2(L(n/2-1))(n/2))(n)
=2L(n/2-1)(n)
=Θ(nlgn)

随机化快速排序

既然我们知道了快速排序的最差情况是给的序列已经是顺序或倒序的,那么我们可以将给的序列用随机数打乱顺序(或者随机选择pivot基准数),这样运行时间不依赖于输入序列的顺序,最差的情况由随机数产生器决定,时间复杂度为nlgn。


线性时间排序

我们可以将所有的比较排序转化成决策树:

  • 步骤:

为每一个n值绘制一个决策树。
进行比较时把树分为两个分支,左子树和右子树。
决策树会把所有比较结果得到的排序都列出来。
决策树指出了算法执行过程中所有可能的路线。
决策树只针对确定性的算法,不适用于随机化的算法。

  • 运行时间:

运行时间取决于比较的次数,所有比较的次数会决定决策的高度。
最坏情况的路线会为决策树最大高度。

  • 证明决策树的最小高度要大于 n l o g n nlogn nlogn
 决策树的叶节点数>=n! 
 对于高度为h的决策树:
 叶节点数<=2^h 
 则有n!<=2^h 
 h>=lg(n!)
 h>=lg((n/e)^n)(斯特林(stirling)公式) 
 h=nlg(n/e)
 h=n(lgn-lge) 
 h=Ω(nlgn)

由此可知道,比较排序的最小时间复杂度即为 n l g n nlgn nlgn,因此在基于比较的排序算法中,归并排序和堆排序是渐进最优的,随机化快速排序对于每一个n会有不同的决策树,它们按概率出现,但所有决策树都符合上述证明过程,所以也是渐进最优(asymptotically optimal)的。

计数排序(counting sort)

计数排序是一种线性时间排序,适用于小范围数据的排序,是稳定性排序算法,需要辅助存储数列 C [ 1 , … , k ] C[1,…,k] C[1,,k]。( k k k为输入序列中数的最大值,输入可以有重复值)

pseudocode:
        for i ← 1 to k            //值初始化C数组,全为0
            do C[i]0
        for j ← 1 to n           //在C数组中统计对应下标值在A中出现的次数
            do C[A[j]] ← C[A[j]]+1
        for i ← 2 to k            //C数组中计算小于等于下标值在A中出现次数(前n项和)
            do C[i] ← C[i]+C[i-1]
        for j ← n downto 1       //根据C数组在B数组中排序(保持稳定)
            do B[C[A[j]]] ← A[j]
                C[A[j]] ← C[A[j]]-1  //每排一个数,对应的计数器减一

举例:

    A=4 1 3 4 3         C=1 0 2 2
                        C‘=1 1 3 5
    B=0 0 3 0 0         C‘=1 1 2 5
    B=0 0 3 0 4         C‘=1 1 2 4
    B=0 3 3 0 4         C‘=1 1 1 4
    B=1 3 3 0 4         C‘=0 1 1 4
    B=1 3 3 4 4         C‘=0 1 1 3

时间复杂度为 O ( n + k ) O(n+k) O(n+k),当 k < = n k<=n k<=n时,这个算法效率比较高。

基数排序(radix sort)

基数排序是一种非比较排序,适用于大范围数据的排序,从最后一位开始,依次按位排序(必须采用稳定性的排序):先从最低位开始,分类装入桶中,然后将桶中的局部有序的数组按顺序放回原数组,再按高一位的数进行装桶、放回,直到结束。

举例:

   ----------------------------->
    329     720     720     329
    457     355     329     355
    657     436     436     436
    839     457     839     457
    436     657     355     657
    720     329     457     720
    355     839     657     839

算法正确性证明(归纳法):

  1. 基本情况是,只有一位数字的数组,对它进行排序即可完成数组排序。
  2. 对第 t t t位的排序,应有前 t − 1 t-1 t1位都已经进行了排序的前提假设,在对第t位进行排序时会有两种情况,当两个数字的第t位相同时,对第 t t t位的稳定排序即可保证它们的相对位置不变,排序会按照 t − 1 t-1 t1位的数字进行排序,符合排序要求,当两个数字的第t位不相同时,对第 t t t位的进行排序后的结果同样符合排序要求,所以该算法可以实现数组的排序。

算法一般性描述分析:

  1. 每一轮用计数排序。
  2. 假设数字用b位数表示 数的范围为0~ 2 b 2^b 2b-1。
  3. 将b位数拆分为b/r位数字 b/r即为要处理的轮数。
  4. 运行时间为 O ( b / r ⋅ ( n + k ) ) = O ( b / r ⋅ ( n + 2 r O(b/r·(n+k))=O(b/r·(n+2^r O(b/r(n+k))=O(b/r(n+2r)) 。
  5. b / r ⋅ ( n + 2 r b/r·(n+2^r b/r(n+2r)中的r求导,求导数为0时的解,以获得最小值。
  6. 可知 2 r < = n 2^r<=n 2r<=n,r尽量取大值时 r = l g n r=lgn r=lgn
  7. 此时运行时间为O(bn/lgn) 如果数字范围为 0 − ( 2 b − 1 ) 或 是 说 0 − ( n d − 1 ) 0-(2^b-1)或是说0 -(n^d-1) 0(2b1)0(nd1)

那么运行时间为 O ( d n ) O(dn) O(dn) 如果 d = O ( 1 ) d=O(1) d=O(1)那么运行时间为 O ( n ) O(n) O(n)

因为计数排序在辅助空间的分配使用方面会有时间消耗,所以在实践中基数排序速度并不是很快。对于一字长的数组成的数组,并且可以在线性时间内操作一个字,目前已知的最好排序算法的期望运行时间为 O ( n ∗ ( l g ( l g n ) ) 1 / 2 ) O(n*(lg(lgn))^{1/2}) O(n(lg(lgn))1/2)


顺序统计(选择问题)

定义函数 R a n d − S e l e c t ( A , p , q , i ) Rand-Select(A,p,q,i) RandSelect(A,p,q,i):用分治策略在数组 A [ p . . q ] A[p..q] A[p..q]中获得 第i小 的数。

  1. 如果 p = q p=q p=q则返回 A [ p ] A[p] A[p]

  2. r ← R a n d − P a r t i t i o n ( A , p , q ) r ← Rand-Partition(A,p,q) rRandPartition(A,p,q),随机在 A [ p . . q ] A[p..q] A[p..q]中选一个元素与第一个元素交换,并将其作为划分元素进行一次划分。

  3. k ← r − p + 1 k ← r-p+1 krp+1 k k k是划分元素的序号。
    r r r的左边和右边分别是小于和大于 r r r元素的数。

在这里插入图片描述

  1. 开始递归:
如果i == k即为所求值,返回A[k]。
如果i < k那么递归划分左子数组,返回Rand-Select(A,p,r-1,i)。
如果i > k那么递归划分右子数组,返回Rand-Select(A,r+1,q,i-k)//忽略左边,找右边第i-k小的元素

第k小元素cpp算法实现:

//  main.cpp
//  Created by LilHoe on 2020/9/21.

#include <iostream>
#include <vector>
#include <algorithm>//sort函数包含的头文件
using namespace std;

int partiiton(vector<int> &arr, int p, int q){
   
    int pivot = arr[p];
    int index = p;
    for(int j=index+1;j<=q;j++){
   
        if(arr[j]>=pivot){
   
            continue;
        }
        else{
   
            ++index;
            swap(arr[j], arr[index]);
        }
    }
    swap(arr[index], arr[p]);
    return index;
}

int randomized_Partition(vector<int> &arr, int p, int q){
   
    srand((int)time(0));  // 产生随机种子
    int rand_num = (rand()%(q-p+1))+p;
    swap(arr[p], arr[rand_num]);
    return partiiton(arr, p, q);
}

int rand_select(vector<int> &arr,int p,int q,int i){
   
    if(p==q){
   
        return arr[q];
    }
    int r = randomized_Partition(arr, p, q);
    int k = r-p+1;
    if(k==i){
   
        return arr[r];
    }
    else if (i<k){
   
        return rand_select(arr, p, r-1, i);
    }
    else{
   
        return rand_select(arr, r+1, q, i-k);
    }
}

int main(int argc, const char * argv[]) {
   
    cout<<"输入一组各不相同的数(用空格隔开):"<<endl;
//    int A[8] = {6,10,13,3,7,4,9,2};
//    vector<int> arr(A,A+8);
    vector<int> arr;
    int temp,i;
    while(cin>>temp){
   
        if(cin.get() == '\n'){
   
            break;
        }
        arr.push_back(temp);
    }
    arr.push_back(temp);
    int len = int(arr.size());
//    for(int ind=0;ind<len;ind++){
   
//        cout<<arr[ind]<<" ";
//    }
//    cout<<endl;
    while(true){
   
        cout<<"输入你想找上面序列中第几小的数:"<<endl;
        cin>>i;
        if(i>len){
   
            cout<<"输入的序号大于序列的数量!"<<endl;
        }
        else{
   
            break;
        }
    }
    int p = 0;
    int q = len - 1;
    int result = rand_select(arr, p, q, i);
    cout<<"您输入的序列中第"<<i<<"小的数是:"<<result<<endl;
    return 0;
}
  • 时间复杂度分析:

    • 好的情况:(最好的情况是划分元素左右元素个数相同,可以感受到任意常数比例的划分如1/10都和1/2的效果一样)。若按1/10 9/10划分:
      T ( n ) < = T ( 9 / 10 ⋅ n ) &#
  • 5
    点赞
  • 25
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值