數據結構與算法(八),查找

前面介紹了基本的排序算法,排序一般是查找的前奏操做。這篇介紹基本的查找算法。java

一、符號表

符號表(Symbol Table)是一種存儲鍵值對的數據結構,它能夠將鍵和值關聯起來。支持兩種操做:插入,將一組新的鍵值對插入到表中;查找,根據給定的鍵獲得響應的值。node

符號表,有時又稱索引,是爲了加快查找速度而設計。它將關鍵字Key和記錄Value相關聯,經過關鍵字Key來查找記錄Value。在現實生活中,咱們常常會遇到各類須要根據key來查找value的狀況,好比DNS根據域名查找IP地址,圖書館根據索引號查找圖書等等:算法

符號表的特徵:數據結構

  • 表中不能有重複的鍵
  • 鍵和值不能爲空

符號表的抽象數據類型:ide

public interface ST<K, V> {
    //將鍵值對存入表中
    void put(K key, V value);
    //獲取key對應的值
    V get(K key);
}

二、順序查找

順序查找(Sequential Search)又稱線性查找,是最基本的查找技術。從表中第一個記錄開始,逐個進行查找,若記錄的關鍵字和給定值相等,則查找成功。若直到最後,沒有關鍵字和給定值相等,則查找失敗。性能

代碼:this

public class SequentialST<K, V> implements ST<K, V>{
    private Node head;
    private class Node {
        K key;
        V value;
        Node next;
        public Node(K key, V value, Node next) {
            super();
            this.key = key;
            this.value = value;
            this.next = next;
        }
    }

    @Override
    public void put(K key, V value) {
        Node temp = sequentialSearch(key);
        if(temp != null) {
            temp.value = value;
        }else {
            head = new Node(key, value, head);
        }
    }
    
    //順序查找,【關鍵】
    private Node sequentialSearch(K key) {
        for(Node cur= head; cur != null; cur=cur.next) {
            if(key.equals(cur.key)) {
                return cur;
            }
        }
        return null;
    }

    @Override
    public V get(K key) {
        Node temp = sequentialSearch(key);
        if(temp != null) {
            return temp.value;
        }
        return null;
    }
    
    public static void main(String[] args) {
        SequentialST<String, Integer> st = new SequentialST<>();
        st.put("AA", 2);
        st.put("BB", 2);
        System.out.println(st.get("BB"));
    }
}

很顯然順序查找的時間複雜度爲O(N),效率很是低。設計

三、二分查找

二分查找(Binary Search),又稱折半查找。二分查找的前提是符號表中的記錄必須有序。在符號表中取中間記錄做爲比較對象,若中間值和給定值相等,則查找成功;若給定值小於中間值,則在左半區繼續查找,不然在右半區進行查找;不斷重複直到成功或失敗。3d

代碼:code

/**
 *基於二分查找的符號表 
 */
public class BinarySearchST<K extends Comparable<K>, V> 
    implements ST<K, V> {
    
    private K[] keys;
    private V[] values;
    private int size;
    
    public BinarySearchST(int capacity) {
        keys = (K[]) new Comparable[capacity];
        values = (V[]) new Object[capacity];
    }

    @Override
    public void put(K key, V value) {
        int i = binarySearch(key, 0, size-1);
        //查找到給定的鍵,則更新對應的值, size=0時,i=0
        if(i < size && keys[i].compareTo(key) == 0) {
            values[i] = value;
            return;
        }
        for(int j=size; j>i; j--) {
            keys[j] = keys[j-1];
            values[j] = values[j-1];
        }
        keys[i] = key;
        values[i] = value;
        size++;
    }

    @Override
    public V get(K key) {
        int i = binarySearch(key, 0, size-1);
        if(keys[i].compareTo(key) == 0) {
            return values[i];
        }
        return null;
    }
    
    //二分查找,【關鍵】
    private int binarySearch(K key, int down, int up) {
        while(down <= up) {
            int mid = down + (up-down)/2;
            int temp = keys[mid].compareTo(key);
            if(temp > 0) {
                up = mid-1;
            }else if(temp < 0) {
                down = mid + 1;
            } else {
                return mid;
            }
        }
        return down;
    }
    
    public static void main(String[] args) {
        BinarySearchST<String, Integer> st = new BinarySearchST<>(10);
        st.put("AA", 2);
        st.put("BB", 2);
        System.out.println(st.get("BB"));
    }
}

二分查找的時間複雜度爲O(logN)

四、插值查找

插值查找(Interpolation Search)是根據要查找的關鍵字key與查找表中最大最小記錄的關鍵字比較後的查找方法。其前提條件是符號表有序。

插值查找的關鍵是將二分查找中

代碼:

private int binarySearch(K key, int down, int up) {
    while(down <= up) {
        int mid = down + (key-keys[down])/(keys[up]-keys[down])*(up-down);
        int temp = keys[mid].compareTo(key);
        if(temp > 0) {
            up = mid-1;
        }else if(temp < 0) {
            down = mid + 1;
        } else {
            return mid;
        }
    }
    return down;
}

對於表長較大,且關鍵字分佈比較均勻的符號表,插值查找的性能比二分查找要好的多。

五、二叉查找樹

二叉查找樹(Binary Search Tree),又稱二叉排序樹。它是一棵二叉樹,其中每一個結點的鍵都大於其左子樹中任意結點的鍵而小於其右子樹中任意結點的鍵。

代碼:

//二叉查找樹
public class BST <K extends Comparable<K>, V> 
    implements ST<K, V>  {
    private Node root; //二叉樹的根結點
    
    private class Node {
        K key;  //鍵
        V value; //值
        Node left, right; //左右子樹
        int N; //以該結點爲根的結點總數
        public Node(K key, V value, int n) {
            this.key = key;
            this.value = value;
            N = n;
        }
    }

    @Override
    public void put(K key, V value) {
        root = put(root, key, value);
    }
    
    //插入操做
    private Node put(Node node, K key, V value) {
        if(node == null) 
            return new Node(key,value,1);
        int cmp = key.compareTo(node.key);
        if(cmp < 0) {
            node.left = put(node.left, key, value);
        }else if(cmp > 0) {
            node.right = put(node.right, key, value);
        } else {
            node.value = value;
        }
        node.N = node.left.N + node.right.N + 1; //遞歸返回時更新N
        return node;
    }

    @Override
    public V get(K key) {
        return get(root, key);
    }
    
    //查找操做
    private V get(Node node, K key) {
        if(node == null)
            return null;
        int cmp = key.compareTo(node.key);
        if(cmp < 0) {
             return get(node.left, key);
        }else if(cmp > 0) {
            return get(node.right, key);
        } else {
            return node.value;
        }
    }
}

插入過程:

在插入操做中,若樹爲空,就返回一個含有該鍵值對的新結點,若查找的鍵小於根結點,則在左子樹中插入該鍵,不然在右子樹中插入該鍵。這樣經過遞歸的方法就能構造出一個二叉查找樹。

查找過程:

對於查找操做可使用非遞歸的方法來提升性能。其代碼爲:

@Override
public V get(K key) {
    Node node = root;
    while(node != null) {
        int cmp = key.compareTo(node.key);
        if(cmp == 0) {
            return node.value;
        }else if(cmp > 0) {
            node = node.right;
        }else {
            node = node.left;
        }
    }
    return null;
}

刪除操做

若要刪除的結點是二叉樹中的葉子結點,刪除它們對整棵樹無影響,直接刪除便可。若刪除的結點是隻有左子樹或右子樹,將它的左子樹或右子樹整個移動到刪除結點的位置便可。如刪除二叉樹中最小結點的過程:

若要刪除的的結點 既有左子樹又有右子樹,只需找到要刪除結點的直接前驅或直接後繼(即左上樹的最大結點或右子樹的最小結點)S,用S來替換它 ,而後刪除結點S便可。如刪除帶左右子樹的結點2的過程:

代碼以下:

//刪除鍵key及其對應的值
public void delete(K key) {
    root = delete(root, key);
}

private Node delete(Node node, K key) {
    if(node == null) 
        return null;
    int cmp = key.compareTo(node.key);
    if(cmp < 0) {
        node.left = delete(node.left, key);
    }else if(cmp > 0) {
        node.right = delete(node.right, key);
    } else {
        if(node.left == null) {
            return node.right;
        }
        if(node.right == null) {
            return node.left;
        }
        Node temp = node;
        node = min(temp.right);
        node.right = deleteMin(temp.right);
        node.left = temp.left;
    }
    node.N = node.left.N + node.right.N + 1; //從棧返回時更新N
    return node;
}

//刪除一個子樹的最小結點
private Node deleteMin(Node node) {
    if(node.left == null) { //刪除結點node
        return node.right;
    }
    node.left = deleteMin(node.left);
    node.N = node.left.N + node.right.N + 1; //更新子樹的計數N
    return node;
}

//查找一個子樹的最小結點
private Node min(Node node) {
    if(node.left == null) {
        return node;
    }
    return min(node.left);
}

二叉查找樹的性能:

二叉查找樹的性能取決於樹的形狀,而樹的形狀取決於鍵被插入的順序。最好狀況下,二叉樹是徹底平衡的,此時查找和插入的時間複雜度都爲O(logN)。最壞狀況下,二叉樹呈線型,此時查找和插入的時間複雜度都爲O(N)。平均狀況下,時間複雜度爲O(logN)。

六、平衡查找樹

雖然二叉查找樹可以很好的用於許多應用中,但它在最壞狀況下的性能很糟糕。而平衡查找樹的全部操做都可以在對數時間完成。

一、平衡二叉樹(AVL樹)

平衡二叉樹(Self-Balancing Binary Search Tree),是一種二叉排序樹,其中每個節點的左子樹和右子樹的高度差至多等於1。將二叉樹上結點的左子樹深度減去右子樹深度的值稱爲平衡因子BF(Balance Factor)。平衡二叉樹的BF值只能爲-1,0,1。

第一幅圖是平衡二叉樹。在第二幅圖中結點2的左結點比結點2大,因此它不是二叉排序樹,而平衡二叉樹的前提是一個二叉排序樹。在第三幅圖中結點5的左子樹的高度爲2,右子樹高度爲0,BF值爲2,不符合平衡二叉樹的定義。

平衡二叉樹的構建思想:在構建二叉查找樹的過程當中,每當插入一個結點時,先檢查是否因插入而破壞了樹的平衡性,如果,則找出最小不平衡子樹,調整最小不平衡子樹中各結點之間的連接關係,進行相應的旋轉,使之成爲新的平衡子樹。

對不平衡子樹的操做有左旋和右旋。

左旋:

代碼:

private Node rotateLeft(Node h) {
    Node x = h.right;
    h.right = x.left;
    x.left = h;
    x.N = h.N;
    h.N = h.left.N + h.right.N + 1;
    return x;
}

右旋:

代碼:

private Node rotateRight(Node h) {
    Node x = h.left;
    h.left = x.right;
    x.right = h;
    x.N = h.N;
    h.N = h.left.N + h.right.N + 1;
    return x;
}

向一棵AVL樹中插入一個結點的四種狀況:

  • 第一種:當一個結點的BF值大於等於2,而且它的子結點的BF值爲正,則右旋
  • 第二種:當一個結點的BF值大於等於2,但它的子結點的BF值爲負,對子結點進行左旋操做,變爲第一種狀況,而後再進行處理。
  • 第三種:當一個結點的BF值小於等於-2,而且它的子結點的BF值爲負,則左旋
  • 第四種:當一個結點的BF值小於等於-2,但它的子結點的BF值爲正,對子結點進行右旋操做,變爲第三種狀況,而後再進行處理。

AVL的實現:

//平衡二叉樹(AVL樹)的實現
public class AVL<K extends Comparable<K>, V> implements ST<K, V> {
    private Node root; // 二叉樹的根結點

    private class Node {
        K key; // 鍵
        V value; // 值
        Node left, right; // 左右子樹
        int N; // 以該結點爲根的結點總數

        public Node(K key, V value, int n) {
            this.key = key;
            this.value = value;
            N = n;
        }
    }

    // 左旋
    private Node rotateLeft(Node h) {
        Node x = h.right;
        h.right = x.left;
        x.left = h;
        x.N = h.N;
        h.N = h.left.N + h.right.N + 1;
        return x;
    }

    // 右旋
    private Node rotateRight(Node h) {
        Node x = h.left;
        h.left = x.right;
        x.right = h;
        x.N = h.N;
        h.N = h.left.N + h.right.N + 1;
        return x;
    }

    @Override
    public void put(K key, V value) {
        root = put(root, key, value);
    }

    // 插入操做
    private Node put(Node node, K key, V value) {
        if (node == null)
            return new Node(key, value, 1);
        int cmp = key.compareTo(node.key);
        if (cmp < 0) {
            node.left = put(node.left, key, value);
        } else if (cmp > 0) {
            node.right = put(node.right, key, value);
        } else {
            node.value = value;
        }

        if (BF(node) <= -2) {
            if (BF(node.right) > 0) {
                rotateRight(node.right);
            }
            rotateLeft(node);
        }
        if (BF(node) >= 2) {
            if (BF(node.left) < 0) {
                rotateLeft(node);
            }
            rotateRight(node);
        }

        node.N = node.left.N + node.right.N + 1; // 從棧返回時更新N
        return node;
    }

    // 平衡因子BF的值
    private int BF(Node node) {
        return depth(node.left) - depth(node.right);
    }

    // 求子樹的深度
    private int depth(Node node) {
        if (node == null)
            return 0;
        return Math.max(depth(node.right), depth(node.left)) + 1;
    }

    // 查找操做,和二叉查找樹相同
    @Override
    public V get(K key) {
        Node node = root;
        while (node != null) {
            int cmp = key.compareTo(node.key);
            if (cmp == 0) {
                return node.value;
            } else if (cmp > 0) {
                node = node.right;
            } else {
                node = node.left;
            }
        }
        return null;
    }
}

二、2-3查找樹

一棵2-3查找樹,或爲一棵空樹,或由如下結點組成:

  • 2-結點:含有一個鍵和兩個連接,左連接指向鍵小於該結點的子樹,右連接指向鍵大於該結點的子樹。
  • 3-結點:含有兩個鍵和三個連接,左連接指向鍵都小於該結點的子樹,中連接指向鍵位於該結點兩個鍵之間的子樹,右連接指向鍵大於該結點的子樹。

查找:

2-3樹的查找操做和平衡二叉樹同樣,從根結點開始,根據比較結果,到相應的子樹中去繼續查找,直到命中或查找失敗。

插入:

向一棵2-3樹中插入一個結點可能的狀況

  • 第一種:向2-結點中插入新鍵,只需用一個3-結點替換2-結點便可。
  • 第二種:向3-結點中插入新鍵,這個3-結點沒有父結點,此時可將其分解爲一個含有3個結點的二叉查找樹。
  • 第三種:向一個父結點爲2-結點的3-結點中插入新鍵,先將其分解爲一個含有3個結點的二叉查找樹,而後將中鍵移到父結點,父結點由2-結點變爲3-結點。
  • 第四種:向一個父結點爲3-結點的3-結點中插入新鍵,和第三種狀況同樣,一直向上分解臨時的4-結點,直到遇到一個2-結點將它替換爲3-結點,或到達3-結點的根,而後直接分解成一個含有3個結點的二叉查找樹。如圖,此時2-3樹依然平衡。

性能:

2-3查找樹的插入和查找操做的時間複雜度不超過 O(logN)。

2-3查找樹須要維護兩種不一樣的結點,實現起來比較複雜,而且在結點的轉換過程當中須要大量的複製操做,這些都將產生額外的開銷,使的算法的性能可能比二叉查找樹要慢。

三、紅黑樹

紅黑樹是對2-3查找樹的一種改進,它使用標準的二叉查找樹和一些額外的信息來表示2-3樹。使用兩個2-結點和『紅連接』來表示一個3-結點。因爲每一個結點都只有一個指向本身的連接,因此能夠在結點中使用boolean值來表示紅連接。

紅黑樹,是知足下列條件的二叉樹:

  • 紅連接均爲左連接,即紅色結點必須爲左結點。
  • 沒有任何結點同時和兩個紅連接相連,即不存在左右子結點都爲紅的結點。
  • 任何空連接到根節點的路徑上的黑連接(黑結點)數量相同。
  • 根結點老是黑色的。

2-3查找樹與紅黑樹的對應關係

紅黑樹的左旋和右旋操做,與平衡二叉樹(AVL樹)的基本相同,只需注意旋轉後的結點顏色的變化便可。其代碼等下會給出。

向一個2-結點中插入結點的狀況:

向一個2-結點中插入結點比較簡單,和平衡二叉樹基本相同,這裏須要注意,第二種狀況,因爲紅黑樹的紅色結點必須爲左結點,因此這裏須要一個左旋操做,將右結點變爲左結點。

向一個3-結點中插入結點的狀況:

向一個3-結點中插入結點,由紅黑樹的定義,結點不能和兩個紅色結點相連而且紅色結點必須爲左結點,因此須要作相應的旋轉操做。這裏須要注意第一種狀況中,對左右結點均爲紅色時的顏色轉換,處理後紅色向上傳遞,這可能會使上層結點的左右結點均爲紅色,這時須要對上層繼續進行顏色轉換,直到根結點或不出現左右結點均爲紅的狀況。

代碼實現:

//紅黑樹的實現
public class RedBlackBST <K extends Comparable<K>, V> implements ST<K, V> {
    private static final boolean RED = true;
    private static final boolean BLACK = false;
    private Node root; // 二叉樹的根結點

    private class Node {
        K key; // 鍵
        V value; // 值
        Node left, right; // 左右子樹
        int N; // 以該結點爲根的結點總數
        //因爲每一個結點都只有一個指向本身的連接,因此能夠在結點中使用boolean值來表示紅連接。
        boolean color; 

        public Node(K key, V value, int n, boolean color) {
            this.key = key;
            this.value = value;
            N = n;
            this.color = color;
        }
    }
    
    private boolean isRed(Node node) {
        if(node == null) 
            return false;
        return node.color == RED;
    }

    // 左旋
    private Node rotateLeft(Node h) {
        Node x = h.right;
        h.right = x.left;
        x.left = h;
        x.color = h.color;
        h.color = RED;
        x.N = h.N;
        h.N = h.left.N + h.right.N + 1;
        return x;
    }
    
    // 右旋
    private Node rotateRight(Node h) {
        Node x = h.left;
        h.left = x.right;
        x.right = h;
        x.color = h.color;
        h.color = RED;
        x.N = h.N;
        h.N = h.left.N + h.right.N + 1;
        return x;
    }

    @Override
    public void put(K key, V value) {
        root = put(root, key, value);
        root.color = BLACK; //根節點老是黑色的,由於它沒有父連接
    }

    // 插入操做
    private Node put(Node node, K key, V value) {
        if (node == null)
            return new Node(key, value, 1,RED);
        int cmp = key.compareTo(node.key);
        if (cmp < 0) {
            node.left = put(node.left, key, value);
        } else if (cmp > 0) {
            node.right = put(node.right, key, value);
        } else {
            node.value = value;
        }

        if(isRed(node.right) && !isRed(node.left)) {
            node = rotateLeft(node);
        }
        if(isRed(node.left) && isRed(node.left.left)) {
            node = rotateRight(node);
        }
        if(isRed(node.left) && isRed(node.right)) {
            flipColors(node);
        }

        node.N = node.left.N + node.right.N + 1; // 從棧返回時更新N
        return node;
    }
    
    //顏色轉換
    private void flipColors(Node node) {
        node.color = RED;
        node.left.color = BLACK;
        node.right.color = BLACK;
    }

    // 查找操做,和二叉查找樹同樣
    @Override
    public V get(K key) {
        Node node = root;
        while (node != null) {
            int cmp = key.compareTo(node.key);
            if (cmp == 0) {
                return node.value;
            } else if (cmp > 0) {
                node = node.right;
            } else {
                node = node.left;
            }
        }
        return null;
    }
}

七、性能比較

性能以下圖:


關於紅黑樹的刪除,今天看了很久,也沒徹底弄明白,這裏就不寫出來誤人子弟了,感興趣的能夠看下這篇博客

相關文章
相關標籤/搜索