關於《算法》第一章的一點總結

第一章 基礎

1.1 基礎編程模型

1.1.1 Java程序的基本結構

  • 原始數據類型:整型(int),浮點型(double),布爾型(boolean),字符型(char)
  • 語句:聲明,賦值,條件,循環,調用,返回。
  • 數組
  • 靜態方法:能夠封裝並重用代碼,使咱們能夠用獨立的模塊開發程序。
  • 字符串
  • 標準輸入/輸出
  • 數據抽象:數據抽象封裝和重用代碼,使咱們能夠定義非原始數據類型,進而支持面向對象編程。

clipboard.png

1.1.2 原始數據類型與表達式

  • 整型(int)
  • 雙精度實數類型(double)
  • 布爾型(boolean)
  • 字符型(char)

clipboard.png

  • 表達式
  • 類型轉換
  • 比較
  • 其餘原始類型:java

    • 64位整數long
    • 16位整數short
    • 16位字符char
    • 8位整數byte
    • 32位單精度實數float

1.1.3 語句

  • 聲明語句
  • 賦值語句
  • 條件語句
  • 循環語句
  • 調用和返回語句

1.1.4 簡便記法

  • 聲明並初始化node

    • 變量在聲明時初始化
  • 隱式賦值算法

    • i++
    • --i
    • i/=2
    • i+=2
    • ...
  • 單語句代碼段編程

    • 條件或循環語句代碼段只有一句語句,花括號能夠省略
  • for語句

clipboard.png

1.1.5 數組

  • 建立數組

    • 聲明數組的名字和類型
    • 建立數組
    • 初始化數組元素
  • 起別名網絡

    • 若是將一個數組變量賦予另外一個變量,那麼兩個變量將會指向同一個數組
      int[] a =new int[N];
      ...
      a[i] = 1234;
      ...
      int[] b = a;
      ...
      b[i] = 5678//a[i]的值也會編程5678

1.1.6 靜態方法

clipboard.png

  • 方法的性質數據結構

    • 方法的參數是按值傳遞:在方法中參數變量的使用方法和局部變量相同,惟一不一樣是參數變量的初始值由調用方提供。方法處理的是參數的值,而非參數自己。在靜態方法中改變一個參數變量的值對調用者無影響。
    • 方法名能夠被重載:例如,Java的Math包使用這種方法給全部的原始數值類型實現了Math.abs()、Math.min()、Math.max()。另外一種用法是爲函數定義兩個版本,一個須要一個參數另外一個則爲該參數提供一個默認值。
    • 方法只能返回一個值,但能夠包含多個返回語句:儘管可能存在多條返回語句,任何靜態方法每次都只會返回一個值,即被執行的第一個條返回語句的參數。
    • 方法能夠產生反作用:void類型的靜態方法會產生反作用(接受輸入、產生輸出、修改數組或改變系統狀態)
  • 遞歸模塊化

    • 編寫遞歸代碼時最重要的有如下三點:函數

      • 遞歸總有一個最簡單的狀況——方法的第一條語句老是一個包含return的條件語句。
      • 遞歸調用老是去嘗試解決一個規模更小的子問題。
      • 遞歸調用的父問題和嘗試解決的子問題之間不該該有交集。工具

        二分查找的遞歸實現
        public static int rank(int key, int[] a)
        {   return rank(key, a, 0, a.length - 1); }
        public static int rank(int key, int[] a, int lo, int hi)
        {   //若是key存在於a[]中,它的索引不會小於lo且不會大於hi
        
            if(lo > hi) return -1;
            int mid = lo + (hi - lo) / 2;
            if(key < a[mid]) return rank(key, a, lo, mid -1 );
            else if(key > a[mid]) return rank(key, a, mid + 1, hi);
            else return mid;
        }
  • 基礎編程模型

    • 靜態方法庫是定義在一個Java類中的一組靜態方法
    • Java開發的基本模式是編寫一個靜態方法庫來完成一個任務
  • 模塊化編程

    • 程序總體的代碼量很大時,每次處理的模塊大小仍然適中
    • 能夠共享和重用代碼而無需從新實現
    • 很容易用改進的實現替換老的實現
    • 能夠爲解決編程問題創建合適的抽象模型
    • 縮小調試範圍
  • API

    • 應用程序接口

      • 提供方法須要知道的全部信息

1.1.8 字符串

  • 自動轉換:Java在鏈接字符串的時候會自動將任意數據類型的值轉換爲字符串:若是加號(+)的一個參數是字符串,那個Java會自動將其餘參數都轉換爲字符串。這樣,經過一個空字符串「 」可將任意數據類型的值轉換爲字符串值。

1.1.9 輸入輸出

  • 重定向與管道

    • 重定向:只須要向啓動程序的命令中加入一個簡單的提示符,就能夠將它的標準輸出重定向至一個文件
    • 管道:將一個程序的輸出重定向爲另外一個程序的輸入

1.1.10 二分查找

import java.util.Arrays;
    public class BinarySearch
    {
        public static int rank(int key, int[] a)
        {    //數組必須是有序的
            int lo = 0;
            int hi = a.length - 1;
            while(lo <= hi)
            {    //被查找的鍵要麼不存在,要麼必然存在與a[lo..hi]之中
                int mid = lo + (hi - lo) / 2;
                if (key < a[mid]) hi = mid -1;
                else if (key > a[mid]) lo = mid + 1;
                else return mid;
            }
            return -1;
        }
        public static void main(String[] args)
        {
            int[] whitelist = In.readInts(args[0]);
            Arrays.sort(whitelist);
            while(!StdIn.isEmpty())
            {    //讀取鍵值,若是不存在於白名單中則將其打印
                int key = StdIn.readInt();
                if(rank(key.whitelist) < 0)
                    StdOut.println(key);
            }
        }
    }

1.2 數據抽象

1.2.1 使用抽象數據類型

  • 抽象數據類型(ADT)的定義和靜態方法庫共同之處:

    • 二者的實現均爲Java類
    • 實例方法可能接受0個或多個指定類型的參數,由括號表示而且逗號分隔;
    • 它們可能返回一個指定類型的值,也能不會(用void表示)。
  • 不一樣:

    • API中可能會出現若干個名稱和類型相同且沒有返回值的函數。稱爲構造函數
    • 實例方法不須要static關鍵字。它們不是靜態方法——它們的目的就是操做該數據類型中的值
    • 某些實例方法的存在是爲了尊重Java的習慣,此類方法稱爲繼承的方法並在API中將它們顯示爲灰色。

1.2.3 抽象數據類型的實現

clipboard.png

  • 實例變量:和靜態方法或局部變量最關鍵的區別:每一個時刻局部變量只會有一個值,而每一個實例變量可對應着無數值(數據類型的每一個實例對象都會有一個)。在訪問實例變量時都須要經過一個對象——咱們訪問的是這個對象的值。每一個實例變量的聲明都須要一個可見性修飾符(private:對本類可見)
  • 構造函數:每一個java類至少含有一個構造函數以建立一個對象的標識。 用於初始化實例變量,它能偶直接訪問實例變量且沒有返回值。若是沒有定義構造函數,類將會隱式定義一個默認狀況下不接受任何參數的構造函數並將全部實例變量初始化爲默認值。
  • 實例方法:每一個實例方法都有一個返回值類型、一個簽名(它指定了方法名、返回值類型和全部參數變量的名稱)和一個主體(它有一系列語句組成,包含一個返回語句來說一個返回類型的值傳遞給調用者)。與靜態方法關鍵不一樣:它們能夠訪問並操做實例變量。
  • 能夠經過觸發一個實例方法來操做該對象的值。
  • 做用域:

    • 參數變量:整個方法
    • 局部變量:當前代碼段中它的定義以後的全部語句
    • 實例變量:整個類

1.2.4 更多數據類型的實現

  • 日期
  • 維護多個實現
  • 累加器
  • 可視化累加器

1.2.5 數據類型的設計

  • 封裝

    • 容許

      • 獨立開發用例和實現的代碼
      • 切換至改進的實現而不會影響用例的代碼
      • 支持還沒有編寫的程序(對於後續用例,API可以起到指南的做用)
    • 隔離了數據類型的操做

      • 限制潛在的錯誤
      • 在實現中添加一致性檢查等調試工具
      • 確保用例代碼更清晰
  • 接口繼承:子類型,容許經過指定一個含有一組公共方法的接口爲兩個原本沒有關係的類創建一種聯繫,這兩個類都不準實現這些方法。

    public interface Datable
    {
      int month();
      int day();
      int year();
    }
    public class Date implements Datable
    {
      //實現代碼
    }
  • 實現繼承:子類
  • 等價性:

    • java約定equals()必須是一種等價性關係。它必須具備:

      • 自反性,x.equals(x)爲true
      • 對稱性,當且僅當y.equals(x)爲true時,x.equals(y)返回true
      • 傳遞性,若是x.equals(y)和y.equals(z)均爲true,x.equals(z)也將爲true
    • 另外,它必須接受一個Object爲參數並知足如下性質:

      • 一致性,當兩個對象均未被修改時,反覆調用x.equals(y)老是會返回相同的值
      • 非空性,x.equals(null)老是返回false
      • 不可變性:final只能用來保證原始數據類型的實例變量的不可變性,而沒法用於引用類型的變量。 若是一個應用類型的實例變量含有修飾符final,該實例變量的值(某個對象的引用)永遠沒法改變——它將永遠指向同一個對象,但對象的值自己仍然是可變的。

        public class Vector
        {
          private final double[] coords;
          public Vector(double[] a)
          {
            coords = a;
          }
          ...
        }
        
        用例程序能夠經過給定的數組建立一個Vector對象,並在構造對象執行以後改變Vector中的元素的值:
        double[] a = {3.0, 4.0};
        Vector vector = new Vector(a);
        a[0] = 0.0;//繞過 了公有API
  • 異常(Exception),通常用於處理不受咱們控制的不可預見的錯誤
  • 斷言(Assertion),驗證咱們在代碼中做出的一些假設

1.3 揹包、隊列和棧

1.3.1 集合型抽象數據類型

clipboard.png
clipboard.png

  • 泛型

    • 集合類的抽象數據類型的一個關鍵特性:能夠用它們存儲任意類型的數據,稱爲泛型或參數化類型。API中,類名後的<Item>記號將Item定義爲一個類型參數。它是一個象徵性的佔位符,表示的是用例將會使用的某種具體數據類型。
例如,編寫用棧來處理String對象: 
        java 
        Stack<String> stack = new Stack<String>(); 
        stack.push("Test"); 
        ... 
        String next = stack.pop(); 

使用隊列處理Date對象: 
        java 
        Queue<Date> queue = new Queue<Date>(); 
        queue.enqueue(new Date(12, 31, 1999)); 
        ... 
        Date next = queue.dequeue();
  • 自動裝箱

    • 類型參數必須被實例化爲引用參數。java的封裝類型都是原始數據類型對應的引用類型:Boolean、Byte、Character、Double、Float、Integer、Long和Short分別對應着boolean、byte、character、double、float、integer、long和short。在處理賦值語句、方法的參數和算術或邏輯表達式時,java會自動在引用類型和對應的原始數據類型之間進行轉換。 即自動裝箱與自動拆箱。
java 
        Stack<Integer> stack = new Stack<Integer>(); 
        stack.push(17);//自動裝箱(int -> Integer) 
        int i = stack.pop();//自動拆箱(INteger -> int)
  • 可迭代集合類型

    • 迭代訪問集合中的全部元素
例如,假設用例在Queue中維護一個交易集合 
java 
        Queue<Transaction> collection = new Queue<Transaction>(); 
        //若是集合是可迭代的,用例用一行語句便可打印出交易的列表: 
        for (Transaction t : collection){ StdOut.print(t);} 
這種語法叫foreach語句
  • 揹包

    • 是一種不支持從中刪除元素的集合數據類型——它的目的是幫助用例收集元素並迭代遍歷全部收集到的元素(用例也能夠檢查揹包是否爲空或者獲取揹包中元素的數量)。迭代的順序不肯定且與用例無關。

clipboard.png

簡單的計算輸入中全部double值的平均值和樣本標準差。注意:不須要保存全部的數也能夠計算標準差。

public ckass Stats
{
  public static void main(String[] args)
  {
    Bag<Double> numbers = new Bag<Double>();
    while(!StdIn.isEmpty())
        numbers.add(StdIn.readDouble());
    int N = numbers.size();
    double sum = 0.0;
    for (double x : numbers)
        sum += x;
    double mean = sum/N;
    sum = 0.0;
    for(double x : numbers)
        sum +=(x - mean)*(x - mean);
    double std = Math.sqrt(sum/(N-1));
    StdOut.printf("Mean: %.2f\n", mean);
    StdOut.printf("Std dev: %.2f\n", std);
  }
}
  • 隊列

    • 一種基於先進先出(FIFO)策略的集合類型。用集合保存元素的同時保存它們的相對順序:是它們入列順序和出列順序相同。

clipboard.png

In類的靜態方法readInts()的一種實現,該方法解決的問題:用例無需預先知道文件的大小便可將文件中的全部整數讀入一個數組中。
public static int[] readInts(String name)
{
    In in = new In(name);
    Queue<Integer> q = new Queue<Integer>();
    while (!in.isEmpty())
        q.enqueue(in.readInt());
    int N = q.size();
    int [] a = new int[N];
    for (int i = 0; i < N; i++)
        a[i] = q.dequeue();
    return a;
}
    • 一種基於後進先出(LIFO)策略的集合類型。

clipboard.png

把標準輸入中的全部整數逆序排列,無需預先知道整數的多少。
public class Reverse
{
    public static void main(String[] args)
    {
      Stack<Integer> stack;
      stack = new Stack<Integer>();
      while(!StdIn.isEmpty())
          stack.push(StdIn.readInt());
      for (int i : stack)
          StdOut.println(i);
    }
}
  • Dijikstra的雙棧算術表達式求值算法

    • 將操做數要入操做數棧
    • 將運算符壓入運算符棧
    • 忽略左括號
    • 在遇到右括號時,彈出一個運算符,彈出所需數量的操做數,並將運算符和操做數的運算結果壓入操做數棧。
java 
public class Evaluate 
{ 
    public static void main(String[] args) 
    Stack<String> ops = new Stack<Double>(); 
    while(!StdIn.isEmpty()) 
    {   //讀取字符,若是是運算符則壓入棧
        String s = StdIn.readString(); 
        if (s.equals("(")); 
        else if (s.equals("+")) ops.push(s); 
        else if (s.equals("-")) ops.push(s); 
        else if (s.equals("*")) ops.push(s); 
        else if (s.equals("/")) ops.push(s); 
        else if (s.equals("sqrt")) ops.push(s); 
        else if (s.equals(")")) 
        {   //若是字符爲「)」,彈出運算符和操做數,計算結果並壓入棧
            String op = ops.pop(); 
            double v = vals.pop(); 
            if (op.equals("+")) v = vals.pop() + v; 
            else if (op.equals("+")) v = vals.pop() - v; 
            else if (op.equals("+")) v = vals.pop() * v; 
            else if (op.equals("+")) v = vals.pop() / v; 
            else if (op.equals("+")) v = Math.sqrt(v); 
            vals.push(v) 
        }   //若是字符既非運算符也不是括號,將它做爲double之壓入棧
        else vals.push(Double.parseDouble(s));//字符是數字 
    } 
    StdOut.println(vals.pop()); 
}

1.3.2 集合類數據類型的實現

  • 棧(可以動態調整數組大小的實現)

    • 每項操做的用時與集合大小無關;
    • 空間需求老是不超過集合大小乘以一個常數。
    • 存在缺點:某些push()、pop()操做會調整數組的大小,這項操做的耗時跟棧大小成正比
import java.util.Iterator;
public class ResizingArrayStack<Item> implements Iterable<Item>
{
private Item[] a = (Item[]) new Object[1];//棧元素。java不容許建立泛型數組,所以須要使用類型轉換
private int N = 0;//元素數量
public boolean isEmpty() {return N == 0;}
public int size() {return N;}
private void resize(int max)
{//因爲java數組建立後沒法改變大小,採用建立大小爲max的新數組來替代舊數組的方式動態改變數組實際大小
  Item[] temp = (Item[]) new Object[max];
  for (int i = 0;i < N; i++)
      temp[i] = a[i];
  a = temp;
}
public void push(Item item)
{//將元素添加到棧頂
  if (N == a.length) resize(2*a.length);
  a[N++] = item;
}
public Item pop()
{//從棧頂刪除元素
  Item item = a[--N];
  a[N] = null;//避免對象遊離
  if (N > 0 && N == a.length/4) resize(a.length/2);
  return item;
}
public Iterator<Item> iterator()
{ return new ReverseArrayIterator(); }
private class ReverseArrayIterator implements Iterator<Item>
{//支持後進先出的迭代
  private int i = N;
  public boolean hasNext() { return i > 0;}
  public Item next() { return a[--i];}
  public void remove() { }
}
}

1.3.3 鏈表

  • 鏈表是一種遞歸的數據結構,它或者爲空(null),或者是指向一個結點(node)的引用,該結點含有一個泛型的元素和一個指向另外一條鏈表的引用。
  • 用一個嵌套類來定義節點的抽象數據類型
在須要使用Node類的類中定義它並將它標記爲private,由於它不是爲用例準備的。
private class Node    
{
    Item item;
    Node next;
}

經過new Node()觸發(無參數的)構造函數來建立一個Node類型的對象。調用的結果是一個指向Node對象的引用,它的實例變量均被初始化爲null。Item是一個佔位符,表示咱們但願用鏈表處理的任意數據類型。

  • 構造鏈表

    • 首先爲每一個元素創造一個結點:
    Node first = new Node(); 
    Node second = new Node(); 
    Node thrid = new Node();
    • 將每一個結點的item域設爲所需的值(咱們這裏假設在這些例子中Item爲String):
    first.item = "to"; 
    second.item = "be"; 
    thrid.item = "or";
    • 設置next域來構造鏈表:
    first.next = second; 
    second.next = third;
    • third.next仍然是null,即對象建立時它被初始化的值。
    • third是一條鏈表(它是一個結點的引用,該結點指向null,便是一個空鏈表);

second也是一條鏈表(它是一個結點的引用,且該結點含有一個指向third的引用,而third是一條鏈表)
first也是一條鏈表(它是一個結點的引用,且該結點含有一個指向second的引用,而second是一條鏈表)

clipboard.png

  • 鏈表表示的是一列元素。
  • 插入刪除元素

    • 在表頭插入節點
    • 從表頭刪除結點(該操做只含有一條賦值語句,所以它的運行時間和鏈表長度無關)
    • 在表尾插入結點
    • 其餘位置的插入和刪除操做:使用雙向鏈表,其中每一個結點都好有兩個連接,分別指向不一樣的方向。

clipboard.png

clipboard.png

clipboard.png

  • 棧的實現

    • 它能夠處理任意類型的數據
    • 所需的空間老是和集合的大小成正比
    • 操做所需的時間老是和集合的大小無關

clipboard.png

public class Stack<Item> implements Iterable<Item>
{
    private Node first;//棧頂(最近添加的元素)
    private int N;
    private class Node
    {//定義告終點的嵌套類
      Item item;
      Node next;
    }
    public boolean isEmpty() {return N == 0;}//或:return first == null;
    public int size() {return N;}
    public void push(Item item)
    {//向棧頂添加元素
      Node oldfirst = first;
      first = new Node();
      first.item = item;
      first.next = oldfirst;
      N++;
    }
    public Item pop()
    {
      Item item = first.item;
      first = first.next;
      N--;
      return item;
    }
    //iterator()的實現見揹包實現算法
    public static void main(String[] args)
    {//輸入to be or not to - be - - that - - - is
      Stack<String> s = new Stack<String>();
      while(!StdIn.isEmpty())
      {
        String item = StdIn.readString();
        if(!item.equals("-"))
            s.push(item);
        else if(!s.isEmpty()) StdOut.print(s.pop() + " ");
      }
      StdOut.println("(" + s.size() + " left on stack)");
    }
}
  • 隊列的實現

clipboard.png

public class Queue<Item> implements Iterable<Item>
{
private Node first;
private Node last;
private int N;
private class Node
{
  Item item;
  Node next;
}
public boolean isEmpty() {return N == 0;}//或:return first == null;
public int size() {return N;}
public void enqueue(Item item)
{//向表尾添加元素
  Node oldfirst = last;
  last = new Node();
  last.item = item;
  last.next = null;
  if (isEmpty()) first = last;
  else oldfirst.next = last;
  N++;
}
public Item dequeue()
{//從表頭刪除元素
  Item item = first.item;
  first = first.next;
  if (isEmpty()) last = null;
  N--;
  return item;
}
//
public static void main(String[] args)
{//輸入to be or not to - be - - that - - - is
  Queue<String> s = new Queue<String>();
  while(!StdIn.isEmpty())
  {
    String item = StdIn.readString();
    if(!item.equals("-"))
        q.enqueue(item);
    else if(!q.isEmpty()) StdOut.print(q.dequeue() + " ");
  }
  StdOut.println("(" + q.size() + " left on queue)");
}
}
- 揹包的實現
```
import java.util.Iterator;
public class Bag<Item> implements Iterable<Item>
{
private Node first;
private class Node
{
  Item item;
  Node next;
}
public void add(Item item)
{
  Node oldfirst = first;
  first = new Node();
  first.item = item;
  first.next = oldfirst;
}
//經過遍歷鏈表使Stack、Queue、Bag變爲可迭代的。對於Stack,鏈表的訪問順序是後進先出;Queue,鏈表的訪問順序是先進先出;Bag,後進先出順序,但順序不重要。
public Iterator<Item> iterator()
{ return new ListIterator();}
private class ListIterator implements Iterator<Item>
{
  private Node current = first;
  public boolean hasNext()
  { return current != null;}
  public void remove() { }
  public Item next()
  {
    Item item = current.item;
    current = current.next;
    return item;
  }
}
}
```

1.4 算法分析

1.4.3 數學模型

  • 對於大多數程序,獲得其運行時間的數據模型所需的步驟:

    • 肯定輸入模型,定義問題的規模;
    • 識別內循環(執行最頻繁的語句);
    • 根據內循環中的操做肯定成本模型;
    • 對於給定的輸入,判斷這些操做的執行頻率。
    • 例:二分查找,它的輸入模型是大小爲N的數組a[],內循環是一個while循環中的全部語句,成本模型是比較操做(比較兩個數組元素的值)
    • 白名單,它的輸入模型是白名單的大小N和由標準輸入獲得的M個整數,且假設M>N,內循環是一個while循環中的全部語句,成本模型是比較操做(承自二分查找)

1.4.4 增加數量級的分類

  • 對增加數量級的常見假設的總結

clipboard.png

  • 2-sum NlogN解法(假設全部整數各不相同)

    • 若是二分查找不成功則會返回-1,不會增長計數器的值
    • 若是二分查找返回的 j > i,咱們就有a[i]+a[j]=0,增長計數器的值
    • 若是二分查找返回的j在0和i之間,不能增長計數器,避免重複計數。

      java 
      import java.util.Arrays; 
      public class TwoSumFast 
      { 
          public static int cout(int[] a) 
          { 
              Arrays.sort(a); 
              int N = a.length; 
              int cnt = 0; 
              for (int i = 0; i< N; i++) 
                  if (BinarySearch.rank(-a[i], a) > i) 
                      cnt++; 
              return cnt; 
          } 
      }
  • 3-sum N2logN解法(假設全部整數各不相同)

    import java.util.Arrays;
    public class ThreeSumFast
    {
      public static int cout(int[] a)
      {
        Arrays.sort(a);
        int N = a.length;
        int cnt = 0;
        for (int i = 0; i< N; i++)
            for(int j = i + 1;j < N; j++)
                if (BinarySearch.rank(-a[i]-a[j], a) > j)
                    cnt++;
        return cnt;
      }
    }

1.4.7 注意事項

  • 大常數:例如,當咱們取函數2N2+cN的近似爲 2N2時,咱們的假設是c很小,若是c很大,該近似就是錯誤的。
  • 非決定性的內循環:
  • 指令時間:每條指令執行所需的時間老是相同的假設並不老是正確的。
  • 系統因素:計算機老是同時運行着許多程序
  • 不分伯仲:在咱們比較執行相同任務的兩個程序時,經常出現的狀況是其中一個在某些場景中更快而在另外一些場景中更慢。
  • 對輸入的強烈依賴
  • 多個問題參數

1.4.8 處理對於輸入的依賴

clipboard.png
clipboard.png

1.5 案例研究:union-find算法

  • 優秀的算法由於可以解決實際問題而變得更爲重要;
  • 高效算法的代碼也能夠很簡單;
  • 理解某個實現的性能特色是一項有趣而使人知足的挑戰;
  • 在解決同一個問題的多種算法之間進行選擇時,科學方法是一種重要的工具;
  • 迭代式改進可以讓算法的效率愈來愈高。

1.5.1 動態鏈接性問題

  • 問題的輸入是一列整數對,其中每一個整數都表示一個某種類型的對象,一對整數pq能夠被理解爲「p和q是相連的」,咱們假設相連是一種對等的關係。對等關係可以將對象分爲多個等價類,在這裏,當且僅當兩個對象相連時它們才屬於同一個等價類。咱們的目標是編寫一個程序來過濾掉序列中全部無心義的整數對(兩個整數均來自於同一個等價類中)。換句話說,當程序從輸入中讀取了證書對p q時,若是已知的全部整數對都不能說明p和q相連的,那麼則將這一對整數寫入到輸出中。若是已知的數據能夠說明p 和q是相連的,那麼程序應該忽略p q繼續處理輸入中的下一對整數。

clipboard.png

  • 該問題可應用於:

    • 網絡
    • 變量名等價性
    • 數據集合
  • 設計一份API封裝所需的基本操做:初始化、鏈接兩個觸點、判斷包含某個觸點的份量、判斷兩個觸點是否存在於同一個份量之中以及返回全部份量的數量。

clipboard.png

java 
public class UF 
{ 
    private int[] id;//份量id(以觸點做爲索引) 
    private int count; //份量數量 
    public UF(int N) 
    {//初始化份量id數組 
        count = N; 
        id = new int[N]; 
        for(int i=0;i < N;i++) 
        id[i] = i; 
    } 
    public int count() 
    { return count;} 
    public boolean connected(int p, int q) 
    { renturn find(p) == find(q); } 
    public int find(int p)//見quick-find 
    public void union(int p, int q)//見quick-union,加權quick-union 
    public static void main(String[] args) 
    {//解決由StdIn獲得的動態連通性問題 
        int N = StdIn.readInt() //讀取觸點數量 
        UF N = new UF(N); //初始化N個份量 
        while (!StdIn.isEmpty()) 
        { 
            int p = StdIn.readInt(); 
            int q = StdIn.readInt();//讀取整數對 
            if (uf.connected(p, q)) continue;//若是已經連通則忽略 
            uf.union(p, q);//歸併份量 
            StdOut.println(p + " " + q);//打印鏈接 
        } 
        StdOut.println(uf.count() + "components"); 
    } 
}

1.5.2 實現(均根據以觸點爲索引的id[]數組來肯定兩個觸點是否存在於相同的連通份量中)

  • quick-find算法:保證當且僅當id[p]等於id[q]時p和q是連通的。換句話說,在同一個連通份量重的全部觸點在id[]中的值必須所有相同。

    public int find(int p)
    { return id[p]; }
    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] == pID) id[i] = qID;
    count--;
    }

clipboard.png

  • find()操做的速度顯然是很快的,由於它只須要訪問id[]數組一次。但quick-find算法通常沒法處理大型問題,由於對於每一對輸入union()都須要掃描整個id[]數組。
  • quick-union算法:

    • 每一個觸點所對應的id[]元素都是同一個份量中的另外一個觸點的名稱(也多是它本身)——咱們將這種聯繫稱爲連接
    • 在實現find()方法時,咱們從給定的觸點開始,由它的連接獲得另外一個觸點,再由這個觸點的連接到達第三個觸點,如此繼續指導到達一個根觸點,即連接指向本身的觸點。
    • 當且僅當分別由兩個觸點開始的這個過程到達同一個根觸點時它們存在於同一個連通份量中。

      private int find(int p)
      {//找出份量的名稱
          while(p != id[p]) p = id[p];
              return p;
      }
      public void union(int p, int q)
      {//將p和q的根節點統一
          int pRoot = find(p);
          int qRoot = find(q);
          if (pRoot == qRoot) return;
          id[pRoot] = qRoot;
          count--;
      }

clipboard.png

  • 加權 quick-union算法:記錄每一棵樹的大小並老是將較小的樹鏈接到較大的樹上。

    public class UF
    {
      private int[] id;//父連接數組(由觸點索引)
      private int[] sz;//(有觸點索引的)各個根節點所對應的份量的大小
      private int count; //連通份量的數量
      public WeightedQuickUnionUF(int N)
      {
        count = N;
        id = new int[N];
        for(int i=0;i < N;i++)
          id[i] = i;
        sz = new int[N];
        for(int i = 0; i < N; i++) sz[i] = 1;
      }
      public int count()
      { return count;}
      public boolean connected(int p, int q)
      { renturn find(p) == find(q); }
      public int find(int p)
      {//跟隨連接找到根節點
        while(p != id[p]) p = id[p];
        return p;
      }
      public void union(int p, int q)
      {
        int i = find(p);
        int j = find(q);
        if(i == j) return;
        //將小樹的根節點鏈接到大樹的根節點
        if (sz[i] < sz[j]) { id[i] = j; sz[j] += sz[i];}
        else{id[j] = i;sz[i] += sz[j];}
        count--;
      }
    }
  • 最優算法

clipboard.png

相關文章
相關標籤/搜索