普林斯顿《算法》笔记(三)

简介: 官方网站 官方代码 第三章 查找 3.1 符号表 (Symbol Tables) 符号表是一种存储键值对 (key-value pairs) 的数据结构,其主要目的是将键 (key) 和值 (value) 联系起来。

官方网站 官方代码


第三章 查找


3.1 符号表 (Symbol Tables)

符号表是一种存储键值对 (key-value pairs) 的数据结构,其主要目的是将键 (key) 和值 (value) 联系起来。主要支持两种操作:插入 (put) ,即将一组新的键值对存入存入表中;查找 (get) ,即根据给定的键得到相应的值。

下表列出了符号表的典型应用:




符号表是一种典型的抽象数据类型,它代表一组清晰定义的值以及对这些值相应的操作, 使得我们能够将类型的实现和使用区别开来。下图是一种泛型符号表API:


键的等价性

在Java中所有对象都继承了一个 equals() 方法,Java也为其标准数据类型如Integer、Double和String以及其他一些更加复杂的类型,如File和URL,实现了 equals() 方法。如果是自定义的键则需要重写 equals() 方法 (如1.2.5.8中的Date类型),这时最好使用不可变 (immutable) 的数据类型作为键,如 Integer, Double, String, java.io.File等。如果键是Comparable对象,则 a.compareTo(b) == 0a.equals(b) 是等价的。


有序符号表 (Ordered symbol tables)

典型的应用程序中,键都是Comparable对象,因此可以用 a.compareTo(b) 来比较a和b两个键。这样就能通过Comparable接口带来的键的有序性来更好地实现put() 和 get() 方法,由此也能定义更多实用操作,如下表所示。一般只要见到类的声明中含有泛型变量 Key extends Comparable<Key>,则说明这段程序实在实现这份API。


符号表用例一:

public static void main(String[] args)
{
    ST<String, Integer> st;
    st = new ST<String, Integer>();
    for (int i = 0; !.StdIn.isEmpty(); i++)
    {
        String key = StdIn.readString();
        st.put(key, i);
    }
    for (String s : st.keys())
        StdOut.println(s + " " + st.get(s));
}




符号表用例二:

下列FrequencyCounter 用例统计了标准输入中各个单词的出现频率,然后将频率最高的且不小于指定长度的单词打印出来。

public class FrequencyCounter
{
    public static void main(String[] args)
    {
        int minlen = Integer.parseInt(args[0]);
        ST<String, Integer> st = new ST<String, Integer>();
        while (!StdIn.isEmpty())
        {
            String word = StdIn.readString();
            if (word.length() < minlen) continue;
            if (!st.contains(word)) st.put(word, 1);
            else                    st.put(word, st.get(word) + 1);
        }
        String max = "";
        st.put(max, 0);
        for (String word : st.keys())
            if (st.get(word) > st.get(max))
                max = word;
        StdOut.println(max + " " + st.get(max));
    }
}

/***************************************
% java FrequancyCounter 1 < tinyTale.txt
it 10
***************************************/



无序链表中的顺序查找 (Sequential Search)

顺序查找通过get()方法会顺序地搜索链表查找给定的键,并返回相应的值;put() 方法同样顺序地搜索给定的键,如果找到则更新相关联的值,否则创建一个新结点并将其插入到链表的开头。

import edu.princeton.cs.algs4.Queue;
import edu.princeton.cs.algs4.StdIn;
import edu.princeton.cs.algs4.StdOut;

public class SequentialSearchST<Key, Value>
{
    private int n;
    private Node first;

    private class Node
    {
        private Key key;
        private Value val;
        private Node next;

        public Node(Key key, Value val, Node next)
        {
            this.key = key;
            this.val = val;
            this.next = next;
        }
    }

    public int size()
    { return n; }

    public boolean isEmpty()
    { return size() == 0; }

    public boolean contains(Key key)
    { return get(key) != null; }

    public Value get(Key key)
    {
        for (Node x = first; x != null; x = x.next)
            if (key.equals(x.key))
                return x.val;
        return null;
    }

    public void put(Key key, Value val)
    {
        if (val == null)
        { delete(key); return; }

        for (Node x = first; x != null; x = x.next)
        {
            if (key.equals(x.key))
            { x.val = val; return; }
        }
        first = new Node(key, val, first);
        n++;
    }
    
    public void delete(Key key)
    { first = delete(first, key); }

    private Node delete(Node x, Key key)
    {
        if (key.equals(x.key))
        { n--; return x.next; }
        x.next = delete(x.next, key);
        return x;
    }

    public Iterable<Key> keys()
    {
        Queue<Key> queue = new Queue<Key>();
        for (Node x = first; x != null; x = x.next)
            queue.enqueue(x.key);
        return queue;
    }

    public static void main(String[] args)
    {
        SequentialSearchST<String, Integer> st = new SequentialSearchST<String, Integer>();
        for (int i = 0; !StdIn.isEmpty(); i++)
        {
            String key = StdIn.readString();
            st.put(key, i);
        }
        for (String s : st.keys())
            StdOut.println(s + " " + st.get(s));
    }
}



有序数组中的二分查找 (Binary Search)

下列实现使用两个数组分别保存key 和 value,这样的数据结构是一对平行的数组。需要创建一个Key类型的Comparable对象的数组和一个Value 类型的Object对象的数组,并在构造函数中将它们转化为Key[]Value[]

put() 方法可以保证数组中Comparable 类型的键有序,具体是使用rank() 方法得到键的具体位置,然后将所有更大的键向后移动一格来腾出位置并插入新的键值。

这份实现的核心在于rank()方法,它返回表中小于给定键的键的数量。以下是递归版本的代码:

public int rank(Key key, int lo, int hi)
{
    if (hi < lo) return lo;
    int mid = (lo + hi) / 2;
    int cmp = key.compareTo(keys[mid]);
    if (cmp < 0) return rank(key, lo, mid-1);
    else if (cmp > 0) return rank(key, mid+1, hi);
    else return mid;
}

import edu.princeton.cs.algs4.Queue;
import edu.princeton.cs.algs4.StdIn;
import edu.princeton.cs.algs4.StdOut;

public class BinarySearchST<Key extends Comparable<Key>, Value>
{
    private static final int INIT_CAPACITY = 2;
    private Key[] keys;
    private Value[] vals;
    private int n = 0;

    public BinarySearchST()
    { this(INIT_CAPACITY); }

    public BinarySearchST(int capacity)
    {
        keys = (Key[]) new Comparable[capacity];
        vals = (Value[]) new Object[capacity];
    }

    private void resize(int capacity)
    {
        assert capacity >= n;
        Key[]    tempk = (Key[])   new Comparable[capacity];
        Value[]  tempv = (Value[]) new Object[capacity];
        for (int i = 0; i < n; i++)
        {
            tempk[i] = keys[i];
            tempv[i] = vals[i];
        }
        keys = tempk;
        vals = tempv;
    }

    public int size()
    { return n; }

    public boolean isEmpty()
    { return size() == 0; }

    public boolean contains(Key key)
    { return get(key) != null; }

    public Value get(Key key)
    {
        if (isEmpty()) return null;
        int i = rank(key);
        if (i < n && keys[i].compareTo(key) == 0) return vals[i];
        return null;
    }

    public int rank(Key key)
    {
        int lo = 0, hi = n-1;
        while (lo <= hi)
        {
            int mid = lo + (hi - lo) / 2;
            int cmp = key.compareTo(keys[mid]);
            if      (cmp < 0) hi = mid - 1;
            else if (cmp > 0) lo = mid + 1;
            else return mid; 
        }
        return lo;
    }

    public void put(Key key, Value val)
    {
        if (val == null)
        { delete(key); return; }

        int i = rank(key);

        if (i < n && keys[i].compareTo(key) == 0)
        { vals[i] = val; return; }

        if (n == keys.length) resize(2*keys.length);

        for (int j = n; j > i; j--)
        {
            keys[j] = keys[j-1];
            vals[j] = vals[j-1];
        }

        keys[i] = key;
        vals[i] = val;
        n++;
    }

    public void delete(Key key)
    {
        int i = rank(key);

        if (i == n || keys[i].compareTo(key) != 0)
        { return; }

        for (int j = i; j < n-1; j++)
        {
            keys[j] = keys[j+1];
            vals[j] = vals[j+1];
        }

        n--;
        keys[n] = null;
        vals[n] = null;

        if (n > 0 && n == keys.length/4) resize(keys.length/2);  
    }

    public void deleteMin()
    { delete(min()); }

    public void deleteMax()
    { delete(max()); }

    public Key min()
    { return keys[0]; }

    public Key max()
    { return keys[n-1]; }

    public Key select(int k)
    { return keys[k]; }

    public Key floor(Key key)
    {
        int i = rank(key);
        if (i < n && key.compareTo(keys[i]) == 0) return keys[i];
        if (n == 0) return null;
        else return keys[i-1];
    }

    public Key ceiling(Key key)
    {
        int i = rank(key);
        if (i == n) return null;
        else return keys[i];
    }

    public int size(Key lo, Key hi)
    {
        if (lo.compareTo(hi) > 0) return 0;
        if (contains(hi)) return rank(hi) - rank(lo) + 1;
        else              return rank(hi) - rank(lo);
    }

    public Iterable<Key> keys()
    { return keys(min(), max()); }

    public Iterable<Key> keys(Key lo, Key hi)
    {
        Queue<Key> queue = new Queue<Key>();
        if (lo.compareTo(hi) > 0) return queue;
        for (int i = rank(lo); i < rank(hi); i++)
            queue.enqueue(keys[i]);
        if (contains(hi)) queue.enqueue(keys[rank(hi)]);
        return queue;
    }

    public static void main(String[] args)
    { 
        BinarySearchST<String, Integer> st = new BinarySearchST<String, Integer>();
        for (int i = 0; !StdIn.isEmpty(); i++)
        {
            String key = StdIn.readString();
            st.put(key, i);
        }

        System.out.println(st.size());
        st.delete("S");
        System.out.println(st.floor("Z"));
        System.out.println(st.select(2));
        System.out.println(st.size());

        for (String s : st.keys())
            StdOut.println(s + " " + st.get(s));
    }
}


  • Sequential search 和 binary search 的成本模型,插入都是线性级别的,对于大数组来说太慢了。我们需要一种更复杂的数据结构如二叉查找树 (binary search tree)来实现对数级别的插入和查找操作。


要支持高效的插入操作,需要一种链式结构,但单链表是无法使用binary search的,因为binary search的高效性来源于能够通过索引快速取得任何子数组的中间元素 (但得到一条链表的中间元素的唯一方法是沿链表遍历)。下一节的二叉查找树 (binary search tree)可以将binary search的效率和链表的灵活性结合起来。下表是本章各种符号表实现的优缺点概览:






3.2 二叉查找树 (Binary Search Tree)

二叉树 (binary tree):每个结点最多有两个子树的树结构。

二叉查找树 (binary search tree):二叉查找树的每个结点包含一个Comparable键和一个值,且每个结点的键 (key)都大于其左子树中的任意结点的键而小于右子树的任意结点的键。是一种结合了链表插入的灵活性和有序数组查找的高效性的符号表实现。


3.2.1 查找

查找分为两种情况:如果含有该键的结点存在表中,则返回相应的值;若不存在则返回null。

3.2.2 插入

插入同样分两种情况:如果该键的结点在表中,则更新值;若不存在则增加新结点。

二叉查找树能够保持键的有序性,因此它可以作为实现有序符号表中众多方法的基础。

3.2.3 floor 和 ceiling

3.2.4 Selection

Selection操作是要找到排名为k的键 (即树中正好有k个小于它的键)。

3.2.5 Rank

rank()select()的逆方法,返回给定键的排名。

3.2.6 删除最大键和删除最小键

3.2.7 删除操作

3.2.8 范围查找 (range queries)

要实现能够返回给定范围内键的keys方法,首先要对二叉树进行中序遍历 (inorder traversal)。如下所示:

private void print(Node x)
{
    if (x == null) return;
    print(x.left);
    StdOut.println(x.key);
    print(x.right);
}

范围查找主要通过将所有给定范围内的键加入队列 Queue 来实现。


  • 下列二叉查找树的实现中树由 Node 对象组成,每个对象含有一对键值,两条链接 (link) 和一个结点计数器N。每个Node对象都是一颗含有N个结点的子树的根结点。
import edu.princeton.cs.algs4.StdIn;
import edu.princeton.cs.algs4.StdOut;
import edu.princeton.cs.algs4.Queue;

public class BST<Key extends Comparable<Key>, Value>
{
    private Node root;

    private class Node
    {
        private Key key;
        private Value val;
        private Node left, right;
        private int N;

        public Node(Key key, Value val, int N)
        { this.key = key; this.val = val; this.N = N; }
    }

    public int size()
    { return size(root); }

    private int size(Node x)
    {
        if (x == null) return 0;
        else           return x.N;
    }

    public Value get(Key key)
    { return get(root, key); }

    private Value get(Node x, Key key)
    {
        if (x == null) return null;
        int cmp = key.compareTo(x.key);
        if      (cmp < 0) return get(x.left, key);
        else if (cmp > 0) return get(x.right, key);
        else return x.val;
    }

    public void put(Key key, Value val)
    { root = put(root, key, val); }

    private Node put(Node x, Key key, Value val)
    {
        if (x == null) return new Node(key, val, 1);
        int cmp = key.compareTo(x.key);
        if      (cmp < 0) x.left = put(x.left, key, val);
        else if (cmp > 0) x.right = put(x.right, key, val);
        else x.val = val;
        x.N = size(x.left) + size(x.right) + 1;
        return x;
    }

    public Key min()
    { return min(root).key; }

    private Node min(Node x)
    {
        if (x.left == null) return x;
        return min(x.left);
    }

    public Key max()
    { return max(root).key; }

    private Node max(Node x)
    {
        if (x.right == null) return x;
        return max(x.right);
    }

    public Key floor(Key key)
    {
        Node x = floor(root, key);
        if (x == null) return null;
        return x.key;
    }
    
    private Node floor(Node x, Key key)
    {
        if (x == null) return null;
        int cmp = key.compareTo(x.key);
        if (cmp == 0) return x;
        if (cmp < 0) return floor(x.left, key);
        Node t = floor(x.right, key);
        if (t != null)  return t;
        else            return x;
    }

    public Key select(int k)
    { return select(root, k).key; }

    private Node select(Node x, int k)
    {
        if (x == null) return null;
        int t = size(x.left);
        if      (t > k) return select(x.left, k);
        else if (t < k) return select(x.right, k-t-1);
        else            return x;
    }

    public int rank(Key key)
    { return rank(key, root); }

    private int rank(Key key, Node x)
    {
        if (x == null) return 0;
        int cmp = key.compareTo(x.key);
        if      (cmp < 0) return rank(key, x.left);
        else if (cmp > 0) return 1 + size(x.left) + rank(key, x.right);
        else    return size(x.left);
    }

    public void deleteMin()
    { root = deleteMin(root); }

    private Node deleteMin(Node x)
    {
        if (x.left == null) return x.right;
        x.left = deleteMin(x.left);
        x.N = size(x.left) + size(x.right) + 1;
        return x;
    }

    public void delete(Key key)
    { root = delete(root, key); }

    private Node delete(Node x, Key key)
    {
        if (x == null) return null;
        int cmp = key.compareTo(x.key);
        if      (cmp < 0) x.left = delete(x.left, key);
        else if (cmp > 0) x.right = delete(x.right, key);
        else
        {
            if (x.right == null) return x.left;
            if (x.left == null) return x.right;
            Node t = x;
            x = min(t.right);
            x.right = deleteMin(t.right);
            x.left = t.left;
        }
        x.N = size(x.left) + size(x.right) + 1;
        return x;
    }

    public Iterable<Key> keys()
    { return keys(min(), max()); }

    public Iterable<Key> keys(Key lo, Key hi)
    {
        Queue<Key> queue = new Queue<Key>();
        keys(root, queue, lo, hi);
        return queue;
    }

    private void keys(Node x, Queue<Key> queue, Key lo, Key hi)
    {
        if (x == null) return;
        int cmplo = lo.compareTo(x.key);
        int cmphi = hi.compareTo(x.key);
        if (cmplo < 0) keys(x.left, queue, lo, hi);
        if (cmplo <= 0 && cmphi >= 0) queue.enqueue(x.key);
        if (cmphi >0 ) keys(x.right, queue, lo, hi);
    }

    public static void main(String[] args)
    {
        BST<String, Integer> st = new BST<String, Integer>();
        for (int i = 0; !StdIn.isEmpty(); i++)
        {
            String key = StdIn.readString();
            st.put(key, i);
        }
        for (String s : st.keys())
            StdOut.println(s + " " + st.get(s));
    }
}


各种符号表实现的比较:






3.3 平衡查找树 (Balanced Search Tree)

1、 2-3查找树

一颗2-3查找树或为一颗空树,或由以下结点组成:

  • 2- 结点,含有一个键(key) (及其对应的值) 和两条链接 (link),左链接指向的的键都小于该结点,右链接指向的键都大于该结点。
  • 3- 结点,含有两个键 (及其对应的值) 和三条链接,左链接指向的键都小于该结点,中链接指向的键位于该结点两个键之间,右链接指向的键都大于该结点。

同时将指向一颗空树的链接成为空链接 (null link)。


查找和各种插入操作

这部分直接看图可比文字描述清晰多了。


下图总结了将一个4- 结点分解为一颗2-3 树可能的6种情况。2-3 树插入算法的根本在于这些变换都是局部的:除了相关的结点和链接外不必修改或检查树的其他部分。



这些局部变换不会影响树的全局有序性和平衡性:任意空链接到根结点的路径长度都是相等的。以下图举例,变换前根结点到所有空链接的路径长度为h,变换后仍然为h。只有当根结点被分解为3个2- 结点时,路径长度才会加1。


下图显示了2-3树的平衡性,对于升序插入10个键,二叉查找树会变成高度为9,而2-3 树的高度则为2 。



2、红黑二叉查找树 (Red Black BST)

红黑树的红链接(red link) 将两个2- 结点连接起来构成一个3- 结点,黑链接则是2-3 树的普通链接。红黑树满足以下条件:

  • 红链接均为左链接
  • 没有任何一个结点同时和两条红链接相连
  • 树是完美黑色平衡的,即任意空链接到根结点的路径上的黑链接的数量相同




颜色表示

这里约定空链接为黑色。



旋转


各种插入操作


所有插入步骤都可以归结为一下三种操作:

  • 如果右子结点是红色而左子结点是黑色,进行左旋转。
  • 如果左子结点是红色且它的子结点也是红色,进行右旋转。
  • 如果左右子结点均为红色,则进行颜色变换。

import edu.princeton.cs.algs4.StdIn;
import edu.princeton.cs.algs4.StdOut;
import edu.princeton.cs.algs4.Queue;

public class RedBlackBST<Key extends Comparable<Key>, Value>
{
    private static final boolean RED = true;
    private static final boolean BLACK = false;

    private Node root;

    private class Node
    {
        private Key key;
        private Value val;
        private Node left, right;
        private boolean color;
        private int size;

        public Node(Key key, Value val, boolean color, int size)
        {
            this.key = key;
            this.val = val;
            this.color = color;
            this.size = size;
        }
    }

    private boolean isRed(Node x)
    {
        if (x == null) return false;
        return x.color == RED;
    }

    private int size(Node x)
    {
        if (x == null) return 0;
        return x.size;
    }

    public int size()
    { return size(root); }

    public boolean isEmpty()
    { return root == null; }

    public Value get(Key key)
    { return get(root, key); }

    private Value get(Node x, Key key)
    {
        while (x != null)
        {
            int cmp = key.compareTo(x.key);
            if      (cmp < 0) x = x.left;
            else if (cmp > 0) x = x.right;
            else              return x.val;
        }
        return null;
    }

    public void put(Key key, Value val)
    {
        root = put(root, key, val);
        root.color = BLACK;
    }

    private Node put(Node h, Key key, Value val)
    {
        if (h == null) return new Node(key, val, RED, 1);

        int cmp = key.compareTo(h.key);
        if      (cmp < 0) h.left = put(h.left, key, val);
        else if (cmp > 0) h.right = put(h.right, key, val);
        else              h.val = val;

        if (isRed(h.right) && !isRed(h.left))       h = rotateLeft(h);
        if (isRed(h.left)  && isRed(h.left.left))   h = rotateRight(h);
        if (isRed(h.left)  && isRed(h.right))       flipColors(h);
        h.size = size(h.left) + size(h.right) + 1;

        return h;
    }
    
    public void deleteMin()
    {
        if (!isRed(root.left) && !isRed(root.right))
            root.color = RED;

        root = deleteMin(root);
        if (!isEmpty()) root.color = BLACK;
    }

    private Node deleteMin(Node h)
    {
        if (h.left == null) return null;

        if (!isRed(h.left) && !isRed(h.left.left))
            h = moveRedLeft(h);

        h.left = deleteMin(h.left);
        return balance(h);
    }

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

    private Node rotateRight(Node h) {
        // assert (h != null) && isRed(h.left);
        Node x = h.left;
        h.left = x.right;
        x.right = h;
        x.color = x.right.color;
        x.right.color = RED;
        x.size = h.size;
        h.size = size(h.left) + size(h.right) + 1;
        return x;
    }

    public Key min() {
        return min(root).key;
    } 

    // the smallest key in subtree rooted at x; null if no such key
    private Node min(Node x) { 
        // assert x != null;
        if (x.left == null) return x; 
        else                return min(x.left); 
    } 

 
    public Key max() {
        return max(root).key;
    } 

    private Node max(Node x) { 
        if (x.right == null) return x; 
        else                 return max(x.right); 
    } 


    private void flipColors(Node h)
    {
        h.color = !h.color;
        h.left.color = !h.left.color;
        h.right.color = !h.right.color;
    }

    private Node moveRedLeft(Node h)
    {
        flipColors(h);
        if (isRed(h.right.left))
        {
            h.right = rotateRight(h.right);
            h = rotateLeft(h);
            flipColors(h);
        }
        return h;
    }

    private Node balance(Node h)
    {
        if (isRed(h.right))             h = rotateLeft(h);
        if (isRed(h.left) && isRed(h.left.left)) h = rotateRight(h);
        if (isRed(h.left) && isRed(h.right)) flipColors(h);

        h.size = size(h.left) + size(h.right) + 1;
        return h;
    }

    public Iterable<Key> keys()
    {
        if (isEmpty()) return new Queue<Key>();
        return keys(min(), max());
    }

    public Iterable<Key> keys(Key lo, Key hi)
    {
        Queue<Key> queue = new Queue<Key>();
        keys(root, queue, lo, hi);
        return queue;
    }

    private void keys(Node x, Queue<Key> queue, Key lo, Key hi)
    {
        if (x == null) return;
        int cmplo = lo.compareTo(x.key);
        int cmphi = hi.compareTo(x.key);
        if (cmplo < 0) keys(x.left, queue, lo, hi);
        if (cmplo <= 0 && cmphi >= 0) queue.enqueue(x.key);
        if (cmphi > 0) keys(x.right, queue, lo, hi);
    }

    public static void main(String[] args)
    {
        RedBlackBST<String, Integer> st = new RedBlackBST<String, Integer>();
        for (int i = 0; !StdIn.isEmpty(); i++)
        {
            String key = StdIn.readString();
            st.put(key, i);
        }    
        for (String s : st.keys())
            StdOut.println(s + " " + st.get(s));
        StdOut.println(); 
    }
}

/*******************************
*  % more tinyST.txt
 *  S E A R C H E X A M P L E
 *  
 *  % java RedBlackBST < tinyST.txt
 *  A 8
 *  C 4
 *  E 12
 *  H 5
 *  L 11
 *  M 9
 *  P 10
 *  R 3
 *  S 0
 *  X 7
 *******************************/

红黑树的性质

  • 所有基于红黑树的符号表实现都能保证操作的运行时间为对数级别。
  • 一颗大小为N的红黑树的高度不会超过 \(2lgN\)。红黑树的最坏情况是它所对应的2-3 树最左边的路径结点均为3-结点而其余均为2-结点,这样最左边的路径长度是只包含2- 结点的路径长度(~ \(lgN\))的两倍。


各符号表实现的比较






3.4 散列表 (Hash Table)

散列表(Hash table,也叫哈希表),是根据通过键(key) 直接进行访问值 (value)的数据结构。也就是说,它通过把键映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。

给定表M,存在函数f(key),对任意给定的键 (key),代入函数后若能得到包含该关键字的记录在表中的地址(即索引),则称表M为哈希(Hash)表,函数f(key)为哈希(Hash) 函数。


散列函数

如果有一个能够保存M个键值对的数组,就需要一个能将任意键转化为该数组范围内的索引([0, M-1])的散列函数。散列函数应具有一下特点:

  • 一致性 —— 等价的键必然产生相等的散列值
  • 高效性 —— 计算简便
  • 均匀性 —— 对于任意键映射到每个索引都是等概率的



将整数散列的最常用方法是除留余数法(modular hashing)。选择大小为素数M的数组,对于任意正整数k,计算k除以M的余数。如果M不是素数,散列值可能无法均匀分布。


Java中所有数据类型都继承一个 hashCode() 方法,能返回一个32位整数。每一种数据类型的hashCode()方法必须与equals()是一致的。如果 a.equals(b),那么a.hashCode()的返回值与b.hashCode()的返回值必须是一致的。如果两个对象的hashCode()方法的返回值不同,那么这两个对象是不同的。如果两个对象的hashCode()方法的返回值相同,这两个对象也可能不同,还需要equals()方法判断。


hashCode() 的返回值转化为一个数组索引: 因为需要的是数组索引而不是一个32位整数,因此在实现中会将默认的hashCode()方法和除留余数法结合起来产生一个0到M-1的整数:

private int hash(Key x)
{ return (x.hashCode() & 0x7fffffff) % M; }

这段代码会将符号位(sign bit)屏蔽(将一个32位整数变为一个31位非负整数),然后计算它除以M的余数。



基于拉链法(separate chaining) 的散列表

一个散列函数能将键转化为数组索引,散列算法的第二步是碰撞处理(collision resolution),也就是处理两个或多个键的散列值相同的情况。拉链法将大小为M的数组中的每个元素指向一条链表,链表的每个结点存储了一些键值对,同一条链表中键的散列值都一样,为该链表在散列表中的索引,如下图所示:

import edu.princeton.cs.algs4.StdIn;
import edu.princeton.cs.algs4.StdOut;
import edu.princeton.cs.algs4.SequentialSearchST;
import edu.princeton.cs.algs4.Queue;

public class SeparateChainingHashST<Key, Value>
{
    private static final int INIT_CAPACITY = 4;
    private int N;
    private int M;
    private SequentialSearchST<Key, Value>[] st;

    public SeparateChainingHashST()
    { this(INIT_CAPACITY); }

    public SeparateChainingHashST(int M)
    {
        this.M = M;
        st = (SequentialSearchST<Key, Value>[]) new SequentialSearchST[M];
        for (int i = 0; i < M; i++)
            st[i] = new SequentialSearchST<Key, Value>();
    }

    private void resize(int chains)
    {
        SeparateChainingHashST<Key, Value> temp = new SeparateChainingHashST<Key, Value>(chains);
        for (int i = 0; i < M; i++)
        {
            for (Key key : st[i].keys())
            { temp.put(key, st[i].get(key)); }
        }
        this.M = temp.M;
        this.N = temp.N;
        this.st = temp.st;
    }

    private int hash(Key key)
    { return (key.hashCode() & 0x7fffffff) % M; }

    public boolean contains(Key key)
    { return get(key) != null; }

    public Value get(Key key)
    {
        int i = hash(key);
        return st[i].get(key);
    }

    public void put(Key key, Value val)
    {
        if (val == null)
        {
            delete(key);
            return;
        }

        if (n >= 10*M) resize(2*M);

        int i = hash(key);
        if (!st[i].contains(key)) N++;
        st[i].put(key, val);
    }

    public void delete(Key key)
    {
        int i = hash(key);
        if (st[i].contains(key)) N--;
        st[i].delete(key);

        if (M > INIT_CAPACITY && N <= 2*M) resize(M/2);
    }

    public Iterable<Key> keys()
    {
        Queue<Key> queue = new Queue<Key>();
        for (int i = 0; i < M; i++)
        {
            for (Key key : st[i].keys())
                queue.enqueue(key);
        }
        return queue;
    }

    public static void main(String[] args)
    {
        SeparateChainingHashST<String, Integer> st = new SeparateChainingHashST<String, Integer>();
        for (int i = 0; !StdIn.isEmpty(); i++)
        {
            String key = StdIn.readString();
            st.put(key, i);
        }

        for (String s : st.keys())
            StdOut.println(s + " " + st.get(s));
    }
}


  • 拉链法中链表的平均长度为\(N/M\) ,一张含有M条链表和N个键的散列表中,查找和插入操作所需的比较次数为 \(\sim N/M\) ,这样就比 sequential search 快了M倍。
  • 散列表的大小: M如果太大,则有存在许多空链表浪费内存;如果太小则链表太长而浪费查找时间。典型的做法是选择 \(M \sim N / 5\)
  • 散列最主要的目的是将键均匀地散布开来,因此计算散列后键的顺序信息就消失了,因此不大适合实现有序方法。在键的顺序不是特别重要的应用中,拉链法可能是最快的(也是应用最广泛) 的符号表实现。



基于线性探测法 (linear probing) 的散列表

实现散列表的另一种方式是用大小为M的数组保存N个键值对,其中M > N。我们需要依靠数组中的空位来解决碰撞冲突,基于这种策略的所有方法统称为开放地址(open addressing) 散列表。

线性探测法是开放地址散列表的一种。先使用散列函数键在数组中的索引,检查其中的键和被查找的键是否相同。如果不同则继续查找 (将索引扩大,到达数组结尾时返回数组的开头),知道找到该键或遇到一个空元素。

开放地址类散列表的核心思想是将内存作为散列表中的空元素,而不是将其当做链表,这些空元素可以作为查找结束的标志。下列实现中使用了并行数组,一条保存键,一条保存值。

删除操作:如何线性探测表中删除一个键? 直接将该键所在的位置设为null是不行的,因为这会使得之后的元素无法被找到。可行的方法是将簇中被删除键右侧所有的键重新插入散列表。

import edu.princeton.cs.algs4.StdIn;
import edu.princeton.cs.algs4.StdOut;
import edu.princeton.cs.algs4.Queue;

public class LinearProbingHashST<Key, Value>
{
    private static final int INIT_CAPACITY = 4;
    private int N;
    private int M;
    private Key[] keys;
    private Value[] vals;

    public LinearProbingHashST()
    { this(INIT_CAPACITY); }

    public LinearProbingHashST(int capacity)
    {
        M = capacity;
        N = 0;
        keys = (Key[])   new Object[M];
        vals = (Value[]) new Object[M];
    }

    public int size()
    { return n; }

    public boolean isEmpty()
    { return size() == 0; }

    public boolean contains(Key key)
    { return get(key) != null; }

    private int hash(Key key)
    { return (key.hashCode() & 0x7fffffff) % M; }

    private void resize(int capacity)
    {
        LinearProbingHashST<Key, Value> temp = new LinearProbingHashST<Key, Value>(capacity);
        for (int i = 0; i < m; i++)
        {
            if (keys[i] != null)
            { temp.put(keys[i], vals[i]); }
        }
        keys = temp.keys;
        vals = temp.vals;
        M    = temp.M;
    }

    public void put(Key key, Value val)
    {
        if (N >= M/2) resize(2*M);
        int i;
        for (i = hash(key); keys[i] != null; i = (i + 1) % M)
        {
            if (keys[i].equals(key))
            {
                vals[i] = val;
                return;
            }
        }
        keys[i] = key;
        vals[i] = val;
        n++;
    }

    public Value get(Key key)
    {
        for (int i = hash(key); keys[i] != null; i = (i + 1) % M)
        {
            if (keys[i].equals(key)) return vals[i];    
        }
        return null;
    }

    public Iterable<Key> keys() {
        Queue<Key> queue = new Queue<Key>();
        for (int i = 0; i < m; i++)
            if (keys[i] != null) queue.enqueue(keys[i]);
        return queue;
    }

    public static void main(String[] args) { 
        LinearProbingHashST<String, Integer> st = new LinearProbingHashST<String, Integer>();
        for (int i = 0; !StdIn.isEmpty(); i++) {
            String key = StdIn.readString();
            st.put(key, i);
        }

        // print keys
        for (String s : st.keys()) 
            StdOut.println(s + " " + st.get(s)); 
    }
}



散列表和平衡查找树的比较:

  • Java中的java.util.TreeMapjava.util.HashMap分别是基于红黑树和拉链法的散列表的符号表实现。
  • 相对于二叉查找树,散列表的优点在于代码更简单,且查找时间最优。二叉查找树的优点是抽象结构更简单(不需要设计散列函数),红黑树可以保证在最坏情况下的性能且能够支持的操作更多(如排名、选择、排序和范围查找)。 大多数时候的第一选择是散列表,在其他元素更重要时才会选择红黑树。


各种符号表实现的比较 2:





/

相关文章
|
29天前
|
算法
【❤️算法笔记❤️】-每日一刷-19、删除链表的倒数第 N个结点
【❤️算法笔记❤️】-每日一刷-19、删除链表的倒数第 N个结点
59 1
|
29天前
|
算法 索引
❤️算法笔记❤️-(每日一刷-141、环形链表)
❤️算法笔记❤️-(每日一刷-141、环形链表)
44 0
|
29天前
|
算法
【❤️算法笔记❤️】-(每日一刷-876、单链表的中点)
【❤️算法笔记❤️】-(每日一刷-876、单链表的中点)
41 0
|
29天前
|
算法
【❤️算法笔记❤️】-每日一刷-23、合并 K 个升序链表
【❤️算法笔记❤️】-每日一刷-23、合并 K 个升序链表
30 0
|
28天前
|
算法 API 计算机视觉
人脸识别笔记(一):通过yuface调包(参数量54K更快更小更准的算法) 来实现人脸识别
本文介绍了YuNet系列人脸检测算法的优化和使用,包括YuNet-s和YuNet-n,以及通过yuface库和onnx在不同场景下实现人脸检测的方法。
30 1
|
28天前
|
JSON 算法 数据可视化
测试专项笔记(一): 通过算法能力接口返回的检测结果完成相关指标的计算(目标检测)
这篇文章是关于如何通过算法接口返回的目标检测结果来计算性能指标的笔记。它涵盖了任务描述、指标分析(包括TP、FP、FN、TN、精准率和召回率),接口处理,数据集处理,以及如何使用实用工具进行文件操作和数据可视化。文章还提供了一些Python代码示例,用于处理图像文件、转换数据格式以及计算目标检测的性能指标。
52 0
测试专项笔记(一): 通过算法能力接口返回的检测结果完成相关指标的计算(目标检测)
|
29天前
|
算法
❤️算法笔记❤️-(每日一刷-160、相交链表)
❤️算法笔记❤️-(每日一刷-160、相交链表)
17 1
|
28天前
|
数据可视化 搜索推荐 Python
Leecode 刷题笔记之可视化六大排序算法:冒泡、快速、归并、插入、选择、桶排序
这篇文章是关于LeetCode刷题笔记,主要介绍了六大排序算法(冒泡、快速、归并、插入、选择、桶排序)的Python实现及其可视化过程。
12 0
|
29天前
|
算法
❤️算法笔记❤️-(每日一刷-83、删除排序链表中的重复项)
❤️算法笔记❤️-(每日一刷-83、删除排序链表中的重复项)
29 0
|
29天前
|
算法
❤️算法笔记❤️-(每日一刷-26、删除有序数组的重复项)
❤️算法笔记❤️-(每日一刷-26、删除有序数组的重复项)
23 0
下一篇
无影云桌面