1. 程式人生 > >一週搞定期末考系列之《演算法分析與設計》

一週搞定期末考系列之《演算法分析與設計》

轉眼就到了期末複習演算法的時候了

真的是一點都不慌啊

演算法分析與設計這門課,由於是一門選修課,而且我對演算法分析沒有過多的熱愛,所以沒有對這門課程進行全方位的深度的學習與複習,但是我相信,將下列演算法的全部思想理解清楚後,如果僅僅是為了考試,應該還是不虛的嘿嘿

具體例題思路分析

一、二分搜尋(折半查詢)

從前有一個故事是這樣的,抱著一沓書出圖書館的時候,門響了,在我一本一本的掃描看是那本書沒有借的時候,圖書館大媽過來,麻溜的把我的書分成兩疊,一堆一堆的掃,然後拿著那本《演算法分析與設計》回頭看了我一眼,彷彿在說:二分查詢都不會?(其實這並不是二分查詢嘿嘿)

定義:

優點比較次數少,查詢速度快,平均效能好
缺點要求待查表為有序表,且插入刪除困難
因此,折半查詢方法適用於不經常變動而查詢頻繁的有序列表。

  1. 假設表中元素是按升序排列,將表中間位置記錄的關鍵字與查詢關鍵字比較,如果兩者相等,則查詢成功;
  2. 否則利用中間位置記錄將表分成前、後兩個子表,如果中間位置記錄的關鍵字大於查詢關鍵字,則進一步查詢前一子表,否則進一步查詢後一子表。
  3. 重複以上過程,直到找到滿足條件的記錄,使查詢成功,或直到子表不存在為止,此時查詢不成功。

時間複雜度:

當第一次選取中間位置時,選取的元素等於所要查詢的元素則時間複雜度最短,為O(1)
當在最壞情況下,迴圈被執行O(logn)

演算法實現:

在答主的回答裡面提到了許多二分演算法存在的坑,這也就是為什麼說十個二分九個錯的原因了嘿

這裡我就直接貼上程式碼了

/**
 * Created by Peter Chen on 2017/5/18.
 * 常規演算法
 */
function firstOccurrence(arrays,target){
  var high = arrays.length - 1;
  var low = 0;
  while(low <= high){
    var mid = low + (high - low) / 2;
    if(arrays[mid] >= target) high = mid - 1
; if(arrays[mid] < target) low = mid + 1; } return low; }
/**
 * Created by Peter Chen on 2017/5/18.
 * 遞迴演算法
 */
function firstOccurrenceRecur(arrays,target,high,low){
  if(low > high) return low;
  var mid = low + (high - low) / 2;
  if(arrays[mid] >= target)
    firstOccurrenceRecur(arrays,target,mid - 1,low);
  else{
    firstOccurrenceRecur(arrays,target,high,mid + 1);
  }
}

二、合併排序(歸併排序)

定義:

原理:歸併排序是建立在歸併操作上的一種有效的排序演算法,該演算法是採用分治法(Divide and Conquer)的一個非常典型的應用。將已有序的子序列合併,得到完全有序的序列;即先使每個子序列有序,再使子序列段間有序。若將兩個有序表合併成一個有序表,稱為二路歸併。
優勢:使用它對兩個己有序的序列歸併,將有無比巨大的優勢。其時間複雜度無論是在最好情況下還是在最壞情況下均是O(nlogn),且對資料的有序性不敏感。
缺點:需要與待排序序列一樣多的輔助空間,所以資料節點資料量大,則不適合使用

時間複雜度:

最壞時間複雜度 :O(nlogn)
最好時間複雜度 :O(nlogn)
空間複雜度 :O(n)
與快速排序類似

演算法實現:

原始陣列:{3,8,2,4,1,9,6,5}
初始關鍵字:[3] [8] [2] [4] [1] [9] [6] [5]
一趟歸併後:[3 8] [2 4] [1 9] [5 6]
二趟歸併後:[2 3 4 8] [1 5 6 9]
三趟歸併後:[1 2 3 4 5 6 8 9]

/**
 * Created by Peter Chen on 2017/5/18.
 * 自上而下的歸併排序
 */
function mergeSort(arr) {
  var len = arr.length;
  if(len < 2)
    return arr;
  var mid = Math.floor(len / 2),
    left = arr.slice(0,mid),
    right = arr.slice(mid);
  return merge(mergeSort(left),mergeSort(right));
}

function merge(left,right) {
  var result = [];
  while(left.length > 0 && right.length > 0){
    if(left[0] <= right[0])
      result.push(left.shift());
    else
      result.push(right.shift());
  }
  while(left.length)
    result.push(left.shift());
  while(right.length)
    result.push(right.shift());
  return result;
} 

三、快速排序

定義:

原理:快速排序(Quicksort)是對氣泡排序的一種改進。它的基本思想是:通過一趟排序將要排序的資料分割成獨立的兩部分,其中一部分的所有資料都比另外一部分的所有資料都要小,然後再按此方法對這兩部分資料分別進行快速排序,整個排序過程可以遞迴進行,以此達到整個資料變成有序序列。

優勢:插入排序在對幾乎已經排好序的資料操作時,效率高,即可以達到線性排序的效率。
缺點:不穩定,在序列有序或者逆序的情況下最不利於發揮其長處

時間複雜度:

快速排序的最壞執行情況是O(n²)
平均期望時間是O(nlog n)

演算法實現:

/**
 * Created by Peter Chen on 2017/5/18.
 */
function quickSort(arr,left,right) {
  var len = arr.length,
    partitionIndex;
  left = typeof left != 'number' ? 0 : left;
  right = typeof right != 'number' ? len - 1 : right;
  if(left < right){
    partitionIndex = paitition(arr,left,right);
    quickSort(arr,left,partitionIndex - 1);
    quickSort(arr,partitionIndex + 1,right);
  }
  return arr;
}
function partition(arr,left,right) {//分割槽操作設定基準值
  var pivot = left,
    index = pivot + 1;
  for(i = index;i <= right;i++){
    if(arr[i] < arr[pivot]){
      swap(arr,i,index);
      index++;
    }
  }
  swap(arr,pivot,index - 1);
  return index - 1;
}
function swap(arr,i,j) {
  var temp = arr[i];
  arr[i] = arr[j];
  arr[j] = temp;
}

書本演算法

template <class Type>

void RandomizedQuickSort(Type a[], int p, int r)//給定陣列a,陣列起始和終止位置p,r
{
    if (p < r) {
        int q = RandomizedPartition(a, p, r);//隨機選取起始點,此時已排好第一次
        RandomizedQuickSort(a, p, q - 1);//對前半部分進行遞迴排序
        RandomizedQuickSort(a, q + 1, r);//對後半部分進行遞迴排序
    }
}
int RandomizedPartition(Type a[], int p, int r) {
    int i = Random(p, r);
    Swap(a[i], a[p]);//將選出的元素放置到陣列頭
    return Partition(a, p, r);//進行第一次快速排序
}
int Partition(Type a[], int p, int r) {
    int i = p, i = r + 1;//給哨兵賦初值
    Type x = a[p];//和x進行大小比較
    while (true) {
        while (a[++i] < x && i < r);//從前找到比X大的
        while (a[--j] > x);//從後找到比X小的
        if (i >= j) break;//判斷兩哨兵是否相遇
        Swap(a[i], a[j]);//兩者交換
    }
    a[p] = a[j];
    a[j] = x;//將比較元素放置陣列中間
    return j;//返回第一次快速排序中間位置的元素
}

四、矩陣連乘(動態規劃)

定義:

這裡寫圖片描述
優化思路: A*B*C = (A*B)C = A(B*C)[即尋找最小乘法數]
條件: 具有優化子結構,具有重疊子問題

時間複雜度:

O(p*q*r)=O(n3)
空間複雜性為O(n3)

演算法實現:

思路:建立一個矩陣A[i,j]
每一項m[i,j]=計算Ai~jde 最小乘法數
m[1,n]=計算A1~n的最小乘法數
m[i,i]=計算Ai~i的最小乘法數
m[i,j]=m[i,k]+m[k+1,j]+pi-1pkpj

這裡寫圖片描述

五、最長公共子序列(動態規劃)

LCS(Longest Common Subsequence)

定義:

一個數列 ,如果分別是兩個或多個已知數列的子序列,且是所有符合此條件序列中最長的,則 稱為已知序列的最長公共子序列。

時間複雜度:

(i,j)兩層迴圈,i迴圈m步,j迴圈n步 複雜度為O(mn)

演算法實現:

六、01揹包(動態規劃)

定義:

你穿越到了神祕海域中,發現了各種各樣的寶藏(W1,W2,W3,W4)價值(P1,P2,P3,P4)。但是此時你哥對你說,你只有一個揹包(容量為W),怎麼樣才能把最大價值的東西帶回家咧?並且必須帶完整的物品(01)。
優化思路:其實說到底,也就是你看見東西往不往包裡放而已。
怎麼判斷呢?當然是放了的價值和不放的價值那個更大,取那個對吧。
狀態轉移方程為f[i][v]=max{f[i-1][v](沒放),f[i-1][v-c[i]]+w[i](放了)}(這件物品的體積是c[i],價值是w[i])
條件:具有優化子結構,具有重疊子問題

時間複雜度:

O(VN)

演算法實現:

function max(a, b) {
    return (a > b) ? a : b;
}
function dKnapsack(capacity, size, value, n) {
    //初始化
    var K = [];
    for(var i = 0; i <=capacity + 1; i++) {
        K[i] = [];
    }
    //第一層迴圈商品個數
    for(var i = 0; i <= n; i++) {
        //第二層迴圈揹包容量
        for (var w = 0; w <= capacity; w++) {
            if(i == 0 || w == 0) {//初始為0
                k[i][w] = 0;
            }
            else if (size[i - 1] <= w) {//若可以放入則選擇較大的放入
                K[i][w] = max(value[i - 1] + K[i - 1][w-size[i - 1]],K[i - 1][w]);
            }
            else {//若無法放下,則不放入
                K[i][w] = K[i - 1][w];
            }
            putstr(K[i][w] + " ");
        }
        print();
    }
    return K[n][capacity];
}
//測試
var value = [4, 5, 10, 11, 13];
var size = [3, 4, 7, 8, 9];
var capacity = 16;
var n = 5;
print(dKnapsack(capacity, size, value, n));

執行結果

七、哈夫曼編碼(貪心演算法)

定義: 使用變長碼根據字元出現的頻率進行編碼,可以達到檔案壓縮的效果

字首碼:任意字元的程式碼都不是其他字元

特點:具有貪心選擇性,最優子結構

時間複雜度:

最小堆實現:初始化優先佇列需要O(n),由於最小堆的DeleteMin和Insert運算均需O(logn)時間,n-1次的合併共需要O(nlogn)計算時間,因此,關於n個字元的哈夫曼演算法的計算時間為O(nlogn)

演算法實現:

template <class Type>
class Huffman {
    friend BinaryTree<int> HuffmanTree(Type[], int);
    public :
        operator Type() const { return weight; }
    private:
        BinaryTree<int>tree;
        Type weight;
};

BinaryTree<int>HuffmanTree(Type f[], int n)
{
    //生成單節點樹,並初始化
    Huffman<Type> *w = new Huffman<Type>[n + 1];
    BinaryTree <int>z, zero;
    for (int i = 1; i <= n; i++) {
        z.MakeTree(i, zero, zero);
        w[i].weight = f[i];
        w[i].tree = z;
    }
    //建立優先佇列
    MinHeap<Huffman<Type>>Q(1);
    Q.Initialize(w, n, n);
    //反覆合併最小序頻率樹
    Huffman<Type>x, y;
    for (int i = 1; i < n; i++) {
        Q.DeleteMin(x);
        Q.DeleteMin(y);
        z.MakeTree(0, x.tree, y.tree);
        x.weight += y.weight; x.tree = z;
        Q.Insert(x);
    }
    Q.DeleteMin(x);
    Q.Deactivate();
    delete[]w;
    return x.tree;
}

八、單源最短路徑(Dijkstra演算法)

定義:

一個指定點到其餘各點的最短路徑
這裡寫圖片描述

時間複雜度:

通過矩陣儲存有向圖邊的資料,則時間複雜度為O(N2)

演算法實現:

九、旅行售貨員問題

定義:

時間複雜度:

演算法實現:

十、最小生成樹

定義: 設G=(V,E)是無向連通帶權圖,即一個網路。E中每條邊(v,w)的權為c[v][w]。如果G的一個子圖G·是一棵包含了G的所有頂點的數,則稱G·為G的生成樹。生成樹上各邊權的總和稱為G的最小生成樹。

Prim:按照更新頂點Lowcost排序
Kruskal:按照權遞增排序

時間複雜度:

Prim演算法的時間複雜度為O(n2)
Kruskal演算法的時間複雜度在圖的邊數為e時,為O(eloge)
當e=歐米伽(n2)時,K演算法比P演算法差,但當e=o(n2)時,Kruskal演算法那比Prim演算法好

演算法實現:

PRIM演算法

template<class Type>
void Prim(int n, Type **c)
{
    Type lowcost[maxint];
    int closest[maxint];
    bool s[maxint];
    s[1] = true;
    for (int i = 2; i <= n; i++)
    {
        lowcost[i] = c[1][i];//表示以i為終點的最小權值
        closest[i] = 1;//初始化和1相鄰
        s[i] = false;//都沒連線
    }
    for (int i = 1; i < n; i++) {//重複n-1次,完成演算法
        Type min = inf;
        int j = 1;
        for(int k = 2; k <= n;k++)//遍歷所有沒有連線的點,找出min,並連線
            if ((lowcost[k] < min) && (!s[k])) { min = lowcost[k]; j = k; }
        cout << j << '' << closest[j] << endl;
        s[j] = true;
        for(int k = 2;k <=n;k++)//根據連線的點進行更新
            if ((c[j][k] < lowcost[k] && (!s[k])) { lowcost[k] = c[j][k]; closest[k] = j; }
    }
}

Kruskal

class EdgeNode {
    friend ostream & operator << (ostream&, EdgeNode<Type>);
    friend bool Kruskal(int, int, EdgeeNode<Type>*, EdgeNode<Type>*);
    friend void main(void);
public:
    operator Type() const { return weight; }
private :
    Type weight;
    int u, v;
}

bool Kruskal(int n, int e, EdgeNode<Type>E[], EdgeNode<Type> t[])
{
    MinHeap<EdgeNode<Type>>H(1);
    H.Initialize(E, e, e);
    UnionFind U(n);
    int k = 0;
    while (e && k < n - 1) {//按權遞增,找出連線兩個不同分支的T1T2中的頂點時,用邊形成連通分支
        EdgeNode<int > x;
        H.DeleteMin(x);
        e--;
        int a = U.Find(x, u);
        int b = U.Find(x, v);
        if (a != b) { t[k++] = x; U.Union(a, b); }
    }
    H.Deactivate();
    return (k == n - 1);
}