1. 程式人生 > >演算法(第四版)第一章筆記

演算法(第四版)第一章筆記

第一章 基礎

1.1 基礎程式設計模型  4

1.1.1 Java程式的基本結構  4

1.1.2 原始資料型別與表示式  6

1.1.3  語句  8

1.1.4  簡便記法  9

1.1.5  陣列  10

1.1.6  靜態方法  12

1.1.6.4 遞迴

編寫遞迴程式碼最重要有以下三點:
- 遞迴總有一個最簡單的情況——方法的第一條語句總是一個包含return 的條件語句。
- 遞迴呼叫總是嘗試去解決一個規模更小的子問題,這樣遞迴才能收斂到最簡單的情況
- 遞迴呼叫的父問題 和 嘗試解決的 子問題之間 不應該有交集。

1.1.7  API  16

1.1.7.4 你自己編寫的庫

API的目的是將呼叫和實現分離:除了API中給出的資訊,呼叫者不需要知道實現的其他細節,而實現也不應考慮特殊的應用場景。
- 程式設計師可以將API看做呼叫和實現之間的一份契約,它詳細說明了每個方法的作用。實現的目標就是能夠遵守這份契約。

1.1.8  字串  20

1.1.9 輸入輸出

在我們的模型中,Java程式可以從命令列引數或者一個名為標準輸入流的抽象字元流中獲得輸入,並將輸出寫入另一個為標準輸出流的字元流中。

1.1.9.4 標準輸入

標準輸入流最重要的特點是這些值會在你的程式讀取之後消失。只要程式讀取一個值,它就不能回退並再次讀取它。

1.1.9.5 重定向與管道

1.1.10  二分查詢  28

1.1.11  展望  3

1.1答疑:

  1. 1 / 0 與 1.0 / 0.0 的結果是什麼?
    • 1.0 / 0.0 = INFINITY
    • 1 / 0編譯出錯 除零異常 ,
java.lang.ArithmeticException: / by zero
  1. 一個for迴圈 和 while形式有什麼區別?
    • 答:while 迴圈中的 “遞增變數” 在迴圈結束後還可以繼續使用

習題1.1.25 使用數學歸納法證明歐幾里得演算法

  • 歐幾里得演算法的關鍵在於證明 gcd(a, b) = gcd(b,a mod b) 的正確性
  • 定理:a,b 是整數,則gcd(a, b) = gcd(b,a mod b)

  • 設k,r為整數。設r = a mod b ,則a可表示為 a = r + k*b

  • 假設d 是{a,b}的公約數,則d整除a,b。而r = a - k*b; 所以d整除r, d也是b和r的公約數。
  • 假設d 是{b,r}的公約數,則d整除b,r。而a = r + k*b 所以d也是a,b的公約數。
  • 所以{a,b},{b, r}的公因子集合是一樣的。特別地{a,b}的最大公因子也是{b,r}的最大公因子。即 gcd(a, b) = gcd(b,a mod b)

1.2 資料抽象

資料型別指的是一組值和一組對這些值的操作的集合。
- Java程式設計的基礎主要是使用class關鍵字構造被稱為 引用型別 的資料型別。
- 抽象資料型別(ADT)是一種能夠對使用者隱藏資料表示的資料型別。

1.2.1  使用抽象資料型別

1.2.1.4 物件

  • 物件是能夠承載資料型別的值的實體。
  • 所有物件都有三大特性
    1. 狀態:即型別中的值(例項變數)
    2. 標識:能夠將一個物件區別於另一個物件。可以認為物件的標識就是它在記憶體中的位置(每個類都至少有一個建構函式以建立一個物件的標識)
    3. 行為:資料型別的操作()
  • 引用是訪問物件的一種方式。

1.2.2  抽象資料型別舉例

1.2.3  抽象資料型別的實現

1.2.3.5 API 用例與實現

  • 我們思考的不是應該採取什麼行動來來達成某個計算性的目的,而是用例的需求。按照下面三步走的方式用抽象資料型別來滿足它們。
    • 用例一般需要什麼操作?
    • 資料型別的值應該是什麼才能最好地支援這些操作?
      1. 定義一份API:API的作用是將使用和實現分離,以實現模組化程式設計。
      2. 用一個Java類實現API的定義:首先我們選擇適當的例項變數,然後再編寫建構函式和例項方法。
      3. 實現多個測試用例來驗證前兩步做出的設計決定。

1.2.4  更多抽象資料型別的實現

1.2.4.2 維護多個實現

同一份api的不同實現。
通常採用一種非正式的命名約定
- 通過字首性修飾符區別同一份API的不同實現
- 維護一個沒有字首的參考實現,它應該適合於大多數用例的需求。

1.2.5  資料型別的設計  

抽象資料型別是一種向用例隱藏內部表示的資料型別。

我們提倡的程式設計風格:將大型程式分解為能夠獨立開發和除錯的小型模組(也促進了程式碼複用)。

Java系統的新實現往往更新了多種資料型別的或靜態方法庫的實現,但它們的API並沒有變化。

1.2.5.8 等價性

equals 模板

    @Override
    public boolean equals(Object obj) {
        //如果引用相同,直接返回true 不需要其他測試工作
        if (this == obj) {
            return true;
        }
        //物件為空直接返回false
        if (obj == null) {
            return false;
        }
        //兩個物件的類不同
        if (obj.getClass() != this.getClass()) {
            return false;
        }
//      //書上沒這麼用,還是直接getClass比較好
//      if (!(obj instanceof Date)) {
//          return false;
//      }
        //強制型別
        Date that = (Date)obj;
        if (that.day != day) {
            return false;
        }
        if (that.year != year) {
            return false;
        }
        if (that.mon != mon) {
            return false;
        }
        return true;
    }

1.2.5.10 不可變性

  • 不可變資料型別,該型別的物件的值在建立之後就無法再被改變(final修飾)
    • eg:String
  • 可變資料型別,能夠操作並改變物件中的值
    • eg:陣列

1.2.5.13 斷言

契約式設計的程式設計模型採用的就是斷言的思想。
- 資料型別的設計者需要說明前提條件(呼叫某個方法需要滿足的條件,如:二分查詢需要滿足有序)
- 後置條件(實現在方法返回時必須達到的要求)
- 副作用(方法可能對物件狀態產生的任何變更)

答疑:

要保證含有一個可變的例項變數的資料型別的不可變性,需要得到一個本地副本,稱為保護性複製

1.3揹包、佇列和棧

1.3.1  API  

1.3.1.4 揹包

揹包是一種不支援從中刪除的集合資料型別——它的目的是幫助用例收集元素並迭代遍歷所有收集到的元素。(當然可以檢查揹包是否為空或者獲取其中的數量的功能還是有的)。

  1. 進出棧的順序
    • 進棧的順序的已經定死了
    • abc 依次進。—— a進 … b 進 … c 進 …
      • 那麼區別就只在於進棧之間的出棧元素。
    • 如果後面的元素已經出棧(這裡有一個隱含的條件就是前面的元素已入棧了),那麼前面的未出棧元素一定是逆序出棧。
      • 後元素進了,前元素肯定已經進了(只是不知道出來了沒有)
  2. 入佇列和出佇列的順序

    • 使用Collection類的Iterator,可以方便的遍歷Vector, ArrayList, LinkedList等集合元素,避免通過get()方法遍歷時,針對每一種物件單獨進行編碼。

    • 迭代器模式

1.3.2  集合類資料型別的實現  

參考
- Java目前還不支援建立 泛型陣列。因為java的泛型是擦除實現

List<String>[] l = new ArrayList<String>[10];//錯誤
List<String>[] l = (ArrayList<String>[] )new Object[10];

Item[] a = (Item[]) new Object[N];//正確 

1.3.2.3 調整陣列大小

以棧為栗子:
- push()中,檢查陣列是否太小,如果沒有多餘的空間,就將陣列的長度加倍。

if(N == a.length){
    resize(a.length * 2);
}
  • pop()中, 首先刪除棧頂元素。然後陣列太大就將它的長度減半。
    • 檢測條件為棧的大小是否小於陣列的四分之一。
Item item = a[--N];
a[N] = null;//防止遊離
if(N > 0 && N == a.length / 4){ // 另一條件N > 0 勿忘
    resize(a.length / 2);
}
return item;

調整陣列的函式實現

private void resize(int size){
    Item[] tmp = (Item[]) new Object[size];
    for(int i = 0 ; i < N; i++){
        tmp[i] = a[i];
    }
    a = tmp;
}

1.3.2.4 物件遊離

用陣列實現的棧的栗子:
- 對pop的實現中,被彈出的元素的引用仍然在陣列中,應該將其置為null。

1.3.2.5 迭代

任意可迭代的集合資料型別需要實現的東西
1. 通過實現Iterable介面
- 實現iterator方法,返回Iterator
2. 定義一個實現了Iterator介面的 巢狀內部類
- 實現 hasNext方法
- 實現 next方法
- remove方法可以放空,或者拋異常

陣列實現的迭代 栗子:

@Override
public Iterator<Item> iterator() {
    return new ReverseArrayIterator();
}

private class ReverseArrayIterator<Item> implements Iterator<Item>{
    private int i = N;
    @Override
    public boolean hasNext() {
        return i > 0;
    }

    @Override
    public Item next() {
        return (Item) a[--i];
    }

    public void remove(){

    }
}

連結串列實現的迭代栗子:

private class Itr implements Iterator<Item> {
    Node current = first;
    @Override
    public boolean hasNext() {
        return current != null;
    }

    @Override
    public Item next() {
        Item i = current.item;
        current = current.next;
        return i;
    }
    @Override
    public void remove() {

    }
}

1.3.3  連結串列  

定義:連結串列是一種遞迴的資料結構,或者為空,或者是一個指向一個結點的引用,該結點含有一個泛型元素和一個指向另一條連結串列的引用。

1.3.4  綜述

  • 使用鏈式一般都是從某個固定的點開始訪問。例如樹的根結點。
  • 使用陣列,可以隨機訪問。

在研究一個新的應用領域的時,我們將會按照以下步驟識別目標並使用資料結構物件解決問題
1. 定義API。
2. 根據特定的應用場景開發用例程式碼,先確定客戶怎麼使用(跟以往的思路有些不同)
3. 描述一種資料結構。(一組值的表示), 並在API所對應的抽象資料型別的實現中根據它定義類的例項變數。
4. 描述演算法(實現一組操作方式),並根據它實現類中的例項方法
5. 分析演算法的效能優點。

1.4 演算法分析

時間 空間

1.4.1  科學方法  

  • 所設計的實驗必須是可重現的
  • 所有的假設必須是可證偽的

1.4.2  觀察  

1.4.3  數學模型  

D.E.Knuth 的基本見地:
一個程式執行的時間主要和兩點有關:
- 執行每條語句的耗時
- 取決於計算機、Java編譯器和作業系統
- 執行每條語句的頻率
- 取決程式本身 和 輸入

定義: 我們用~f(N)表示所有隨著N的增大除以f(N)的結果趨近於1的函式。我們用g(N)~f(N)表示g(N)/f(N)的隨著N的·增大趨近於1。

執行最頻繁的指令決定了程式執行的時間,稱這些指令為內迴圈

本書中
- 性質表示需要用實驗驗證的猜想
- 命題表示 在某個成本模型下演算法的數學性質。

附: 迴圈計算

ref

1. 複雜度是線性級別的迴圈體
  • 如果某個迴圈結構以線性方式執行n次,並且迴圈體的時間複雜度都是O(1),那麼該迴圈的複雜度就是O(n).
  • 即使該迴圈跳過某些常數部分,只要跳過的部分是線性的,那麼該迴圈體的時間複雜度仍就是O(n).
    • 下面第二個栗子 以線性方式執行 N/2 次數
for(int i = 0; i < N ; i++){
    //一系列複雜度為O(1)的步驟....
}

for(int i = 0; i < N ; i+=2){
    //一系列複雜度為O(1)的步驟....
}

2. 複雜度是對數級別的迴圈體
for(int i = 0; i < N ; i*=2){
    //一系列複雜度為O(1)的步驟....
}
  • 關鍵概念
      - 迴圈的時間複雜度等於該迴圈體的複雜度乘以迴圈的次數
3. 巢狀迴圈複雜度分析

先計算內層迴圈的時間複雜度,然後用內層的複雜度乘以外層迴圈的次數

3. 方法呼叫的複雜度分析

先計算方法體的的時間複雜度.

1.4.4  增長數量級的分類  

各種級別的對應的經典演算法是什麼

1.4.4.1 常數級別

普通語句 兩個數相加

1.4.4.2 對數級別

對數的底數和增長的數量級無關(因為不同底數僅相當於一個常數因子)

1.4.4.3 線性級別

一維陣列找出最大元素

1.4.4.4 線性對數級別

歸併排序

1.4.4.5 平方級別

檢查所有元素對

1.4.4.6 立方級別

檢查所有三元組

1.4.4.7 指數級別

檢查所有子集

1.4.5  設計更快的演算法  

2sum 計算出陣列中和為0的整數對的數量(假設所有元素都是不同的)
- 先進行排序
- 然後進行二分查詢
- 二分查詢不成功,返回-1,我們不改變計數器的值
- 二分查詢返回的j > i, 計數器的值+1
- 二分查詢的j 在 0 和 i之間,也有a[i] + a[j] = 0;但是不能改變計數器的值,以免重複計數
- 複雜度 NlogN + logN

public int twoSumFast(int[] a){
    Arrays.sort(a);
    int cnt = 0;
    for(int i = 0 ;i<a.length; i++){
        if(Binarysearch.rank(-a[i],a) > i) {
            cnt++;
        }
    }
    return cnt;
}

3sum問題快速解法
- (假設所有元素各不相同)
- 複雜度 N^2logN

public int threeSumFast(int[] a){
    Arrays.sort(a);
    int cnt = 0;
    for(int i = 0; i < a.length; i++){
        for(int j = i+1; j < a.length; i++){
            if(Binarysearch.rank(-(a[i] + a[j]), a) > j) {
                cnt++;
            }
        }
    }
    return cnt;
}

本書中會嘗試按照以下的方式解決各種演算法問題
1. 實現並分析問題的一種簡單解法,通常稱它們為暴力解法
2. 考察演算法的各種改進
3. 用實驗證明新的演算法更快。

1.4.6  倍率實驗  

開發一個輸入生成器來產生實際情況下的各種可能的輸入

  • 例如使用迴圈,每次將問題的輸入規模增長一倍,再呼叫方法
public static double timeTrail(int N) {
    int MAX = 100000;
    int[] a = new int[N];
    for (int i = 0; i < N; i++) {
        a[i] = StdRandom.uniform(-MAX, MAX);
    }
    Stopwatch stopwatch = new Stopwatch();
    ThreeSum.count(a);
    return stopwatch.elapsedTime();
}


public static void main(String[] args) {
    double prev = timeTrail(125);
    for(int N = 250 ; true; N+=N){
        double time = timeTrail(N);
        StdOut.printf("%6d %7.1f ", N , time);
        StdOut.printf("%5.1f\n", time / prev);
        prev = time;
    }
}

命題C (倍率定理) 如果T(N) ~ aN^blgN,那麼T(2N)/T(N) ~ 2^b。
- 注:一般而言,數學模型的對數項是不能忽略的,但在倍率假設中它在預測效能的公式中並不那麼重要。

1.4.7  注意事項  

效能分析無法得到正確的結果
- 一般都是由於我們的猜想基於的一個或多個假設並不完全正確所造成的。

1.4.8  處理對於輸入的依賴  

問題所要處理對輸入建模。困難點:
- 建立輸入模型是不切實際的
- 對輸入的分析可能極端困難

1.4.8.2 對最壞情況下的效能保證

命題D。 在Bag、Stack、Queue的連結串列實現中所有的操作在最壞情況下都是常數級別的

1.4.8.3 隨機化演算法

隨機打亂輸入

1.4.8.4 操作序列

例如:棧。先入棧N個值再將它們彈出所得到的效能 跟 N次壓入彈出的混合操作序列所得到的效能可能是不同的。

1.4.8.5 均攤分析

命題E。在基於可調整大小的陣列實現的Stack資料結構中,對空資料結構所進行的任意操作序列對陣列的平均訪問次數 在最壞情況下 均為常數
- 證明:P125

1.4.9記憶體

型別 所佔位元組數
物件的引用 (一般是一個記憶體的地址) 8
物件開銷 16
boolean 1
byte 1
char 2
int 4
float 4
double 8
long 8
  • 物件開銷包括
    • 一個指向物件的類的引用(Mark word)
    • 垃圾回收資訊
    • 同步資訊
  • 填充位元組用來填充位元組數

    • HotSpot的對齊方式是以8位元組對齊,所有沒有物件最終大小沒有到8個位元組的倍數的,都會被填充
    • 一般記憶體的使用都會被填充為8位元組(64位計算機中的機器字)
  • 當我們說明一個引用所佔的記憶體時,會單獨說明它所指向的物件所佔用的記憶體

1.4.9.2連結串列

巢狀的非靜態(內部)類,還需額外的8個位元組(用於一個指向外部類的引用)

1.4.9.3陣列

分析時,畫出影象,一個一個對應寫出來。

陣列 位元組
物件開銷 16
int 陣列長度 4
填充位元組

eg:

陣列 位元組
物件開銷 16
int 陣列長度 4
填充位元組 4
double 8
double 8
。。。
double 8

- 一個原始資料型別的陣列一般需要24位元組的頭資訊
- 16位元組的物件開銷
- 4位元組(int型別)儲存陣列長度
- 4個填充位元組

  • 一個物件的陣列(一個物件的引用的陣列),一般需要24位元組的頭資訊

小結

型別 位元組數 近似
int[] 24+4N ~4N
double[] 24+8N ~8N
long[] 24+8N ~8N
Date[] 24+8N + 32N ~40N
double[][] 24+8M + (24+8N)*M ~8MN

1.4.9.4字串物件

三個int值:
- 偏移量
- 計數器(字串的長度)
- 雜湊值

//String物件
public class String{
    char[] value;
    int offset;
    int count;
    int hash;
    ....
|
字串物件 位元組
物件開銷 16
字串的值(引用) 8
偏移量 (int) 4
字串的長度(int) 4
雜湊值 (int) 4
填充位元組 4

一個長度為N的String物件一般需要使用40位元組(String物件本身),加上(24+2N)位元組(字元陣列),總共(64+2N)位元組。

呼叫subString方法時,會建立一個新的String物件(40位元組),但是它會重用相同的value陣列(通過偏移量和它的字串長度來指定 ),因此只佔40位元組記憶體。

1.4.10  展望  

  • 不要過早優化。
  • 應該注重寫出正確清晰的程式碼。
  • 但是也不能完全忽略效能

1.5  案例研究:union-find演算法  136

先畫出圖來,再寫程式碼,易理解。
- 用節點(帶標籤的圓圈)表示觸點
- 用一個節點到另一個結點的箭頭表示 連結

由此得到的資料結構的影象表示使我們理解演算法的操作變得相對容易。

1.5.1  動態連通性  136

等價關係能夠將物件分為多個等價類
- 當且僅當兩個物件相連時他們才屬於同一個等價類。

動態連通性的應用場景

1.5.1.1 網路

此程式能夠判定我們是否需要在p和q之間架設一條新的連線才能進行通訊,或是我們可以通過已有的連線在兩者之間建立通訊線路。

1.5.1.2 變數名等同性

在程式中,可以宣告多個引用來指向同一物件,這個時候就可以通過為程式中宣告的引用和實際物件建立動態連通圖來判斷哪些引用實際上是指向同一物件。

對問題的建模

並查集 筆記

對問題進行建模的時候,先儘量想清楚要解決的問題是什麼。
就動態連線性這個場景而言,我們要解決的問題可能是:
- 給出兩個結點,判斷他們是否連通,==如果連通,需要給出具體的路徑==
- union-find 屬於第一種
- 給出兩個結點,判斷他們是否連通,==如果連通,不需要給出具體的路徑==
- 使用基於DFS的演算法

1.5.1.3 數學集合

更高的抽象層次上,可以將輸入的所有整數 看做屬於不同的數學集合。
- 在處理一個整數對p和q時,我們是在判斷它們是否屬於相同的集合
- 如果不是,就將p所屬的集合和q所屬的集合歸併到同一個集合中。

  • 將 整數對 稱為連線
  • 將 物件 稱為觸點
  • 將 等價類 稱為連通分量(簡稱 分量)

union-find的成本模型。在研究實現union-find的API的各種演算法時,我們統計的是陣列的訪問次數(無論讀寫)

對於動態連通圖幾種可能的操作

  • 查詢節點所屬於的組
    • 陣列的位置對應值,即為組號
  • 判斷是否屬於同一個組
    • 兩個組的組號是否相同
  • 連線兩個節點使之屬於同一個組
    • 分別得到兩個節點的組號,組號同時 操作結束;不同時,將其中的一個節點的組號換成另一個節點的組號
  • 獲取組的總數量
    • 初始化為節點的總數。每次成功連線兩個節點之後,遞減1.

API

注意其中使用整數來表示節點,如果需要使用其他的資料型別表示節點,比如使用字串,那麼可以用雜湊表來進行對映,即將String對映成這裡需要的Integer型別。

1.5.2  實現  140

public boolean connected(int p, int q) {
    return find(q) == find(p);
}

public int find(int p) {
    return id[p];
}

// 第一種方法, 當且僅當id[p] 與 id[q]的值相等時,p和q是連通的。
// 即以id[]的值來區分不同的分量。值同就屬於同一個分量,不同就屬於不同的分量
public void union(int p, int q) {
    // 將p和q歸併到到相同的分量中
    int pID = find(p);
    int qID = find(q);
    // 如果p和q在同一個分量之中,則不需要採取任何行動。
    if (pID == qID) {
        return;
    }
    // 將p的分量重新命名為q的名稱
    for (int i = 0; i < id.length; i++) {
        if (id[i] == qID) {
            id[i] = pID;
        }
    }
    count--; //前面“區域性”操作完以後,需要對“全域性”的統計量進行更改
}

1.5.2.1 quick-find演算法

在同一個連通分量中的所有觸點的id[] 中的值必須全部相同。

1.5.2.2 quick-find演算法分析

命題F。在quick-find演算法中,每次find()呼叫只需訪問陣列一次,歸併兩個分量的union操作訪問陣列的次數在(N+3) 和 (2N+1)之間。

1.5.2.3 quick-union演算法

考慮一下,為什麼以上的quick-find 解法會造成“牽一髮而動全身”?因為每個節點所屬的組號都是單獨記錄,各自為政的,沒有將它們以更好的方式組織起來,當涉及到修改的時候,除了逐一通知、修改,別無他法。

所以現在的問題就變成了,如何將節點以更好的方式組織起來,組織的方式有很多種,但是最直觀的還是將組號相同的節點組織在一起,想想所學的資料結構,什麼樣子的資料結構能夠將一些節點給組織起來?常見的就是連結串列,圖,樹,什麼的了。但是哪種結構對於查詢和修改的效率最高?毫無疑問是樹,因此考慮如何將節點和組的關係以樹的形式表現出來。

union與find演算法是互補的。
賦予id[] 陣列的值 不同的意義,每一個觸點所對應的id[]元素 都是同一個分量中另一個的觸點的名稱(也可能是自己)

“根節點”作為連通分量的標識。

//建立連結,每一個觸點所對應的id[]元素 都是同一個分量中另一個的觸點的名稱(也可能是自己)
public int quick_find(int p) {
    //找出分量的名稱
    while (id[p] != p) {
        p = id[p]; 
    }
    return p;
}

//
public void quick_union(int p ,int q) {
    //p和q的根觸點 (類似於樹的根節點)
    int pRoot = quick_find(p);
    int qRoot = quick_find(q);
    if (pRoot == qRoot) {
        return;
    }
    id[pRoot] = qRoot;

    count--;// 全域性的統計量更改
}

1.5.2.4 森林的表示

1.5.2.5 quick-union演算法分析

定義。一棵樹的大小是它節點的數量
- 樹中的一個節點的深度是它到根節點的路徑上的連結數(即 路徑節點總數-1)

命題G。quick-union演算法中的find()方法訪問陣列的次數為1 加上給觸點所對應的節點的深度的兩倍。union和connected 訪問陣列的次數為兩次find操作(如果不在同一個分量中還要加1)

1.5.2.6 加權quick-union演算法

  • 目的:控制樹高。以減少find查詢時間。

  • 新增一個數組和一些程式碼來記錄樹中的節點數,讓比較小(節點數目比較少)的樹的根指向比較大(節點數目多)的樹的根,(減少find查詢時間)改進演算法的效率。

1.5.2.7 加權quick-union演算法的分析

public class WeightedQuickUnion {
    private int[] id;// 父連結陣列,由觸點索引
    private int[] sz;// (由觸點索引的)各個根節點所對應的分量的大小
    private int count;// 連通分量的 數量

    public WeightedQuickUnion(int N) {
        count = N;// 一開始每個節點分屬不同的連通分量
        id = new int[N];
        for (int i = 0; i < id.length; i++) {
            id[i] = i;
        }
        sz = new int[N];
        for (int i = 0; i < sz.length; i++) {
            sz[i] = 1;// 每一個連通分量都只有一個元素,因此均為1
        }
    }

    public int count() {
        return count;
    }

    public int find(int p) {
        // 跟隨連結找到 根節點
        while (p != id[p]) {
            p = id[p];
        }
        return p;
    }

    public void union(int p, int q) {
        int pRoot = find(p);
        int qRoot = find(q);
        if (pRoot == qRoot) {
            return;
        }
        if (sz[pRoot] > sz[qRoot]) {
            id[qRoot] = pRoot;
            sz[pRoot] = sz[pRoot] + sz[qRoot];
        }else if (sz[pRoot] < sz[qRoot]) {
            id[qRoot] = pRoot;
            sz[qRoot] = sz[pRoot] + sz[qRoot];
        }
        count--;//連通後,連通分量總數少1
    }
}

命題H。對N個觸點,加權union演算法構造的森林中的任意節點的深度最多為lgN。

推論。對於加權quick-union演算法和N個觸點,在最壞情況下find connected 和 union 的成本增長數量級為logN。

命題和它的推論的實際意義在於加權quick-union演算法是三種演算法中唯一能解決大型實際問題的演算法。

1.5.2.8 最優演算法

路徑壓縮演算法,每個結點都直接連線到根節點。
- 實現:在檢查節點的同時將它們直接連結到根節點。

public int find(int p){
    int root = p;
    while(root != id[root]){
        root = id[root];
    }
    while(p != root){
        int newP = p;
        id[p] = root;
        p = newP;
    }
    return root;
}