1.算法彙總
首先,來看一張各種字符串查找算法的彙總。前面的文章已經介紹過二叉樹查找和紅黑樹查找。這裏不在介紹。
本文重點介紹後面三種查找算法:線性探測法、R向單詞查找樹和三向單詞查找樹。
2.線性探測法
實現散列表的另一種方式是用大小爲M的數組保存N個鍵值對,其中M>N。依靠數據中的空位解決碰撞衝突。基於這種策略的所有方法都統稱爲開放地址散列表。其中最簡單的方法叫做線性探測法:當碰撞發生時,直接檢查散列表的下一個位置(索引加1),可能產生三種結果:
- 命中,該位置的鍵和被查找的鍵相同;
- 未命中,鍵爲空(該位置沒有鍵);
- 繼續查找,該位置的鍵和被查找的鍵不同。
其核心思想是與其將內存用作鏈表,不如將它們作爲散列表的空元素。即用散列函數找到索引,檢查其中的鍵和被查找的鍵是否相同。如果不同則繼續查找(增加索引,到達數組結尾後再折回數組開頭),直到找到該鍵或者遇到一個空元素。過程如下圖所示:
在基於線性探測法的散列表中執行刪除操作比較複雜,如果將該鍵所在位置爲爲null是不行的。需要將簇中被刪除鍵的右側的所有鍵重新插入散列表。
代碼實現:
//基於線性探測的符號表
public class LinearProbingHashST<Key,Value>
{
private static final int INIT_CAPACITY = 16;
private int n;// 鍵值對數量
private int m;// 散列表的大小
private Key[] keys;// 保存鍵的數組
private Value[] vals;// 保存值的數組
public LinearProbingHashST()
{
this(INIT_CAPACITY);
}
@SuppressWarnings("unchecked")
public LinearProbingHashST(int capacity)
{
this.m = capacity;
keys = (Key[]) new Object[capacity];
vals = (Value[]) new Object[capacity];
}
public int hash(Key key)
{
return (key.hashCode() & 0x7fffffff) % m;
}
public void put(Key key, Value val)
{
if(key == null)
{
throw new NullPointerException("key is null");
}
if(val == null)
{
delete(key);
return;
}
// TODO擴容
if(n >= m/2)
{
resize(2*m);
}
int i = hash(key);
for (; keys[i] != null; i = (i + 1) % m)
{
if(key.equals(keys[i]))
{
vals[i] = val;
return;
}
}
keys[i] = key;
vals[i] = val;
n++;
}
public void delete(Key key)
{
if(key == null)
{
throw new NullPointerException("key is null");
}
if (!contains(key))
{
return;
}
// 找到刪除的位置
int i = hash(key);
while (!key.equals(keys[i]))
{
i = (i + 1) % m;
}
keys[i] = null;
vals[i] = null;
// 將刪除位置後面的值重新散列
i = (i + 1) % m;
for (; keys[i] != null; i = (i + 1) % m)
{
Key keyToRehash = keys[i];
Value valToRehash = vals[i];
keys[i] = null;
vals[i] = null;
n--;
put(keyToRehash, valToRehash);
}
n--;
// TODO縮容
if(n>0 && n == m/8)
{
resize(m/2);
}
}
public Value get(Key key)
{
if(key == null)
{
throw new NullPointerException("key is null");
}
for (int i = hash(key); keys[i] != null; i = (i + 1) % m)
{
if(key.equals(keys[i]))
{
return vals[i];
}
}
return null;
}
public boolean contains(Key key)
{
if(key == null)
{
throw new NullPointerException("key is null");
}
return get(key) != null;
}
private void resize(int cap)
{
LinearProbingHashST<Key,Value> t;
t = new LinearProbingHashST<Key,Value>(cap);
for(int i = 0; i < m; i++)
{
if(keys[i] != null)
{
t.put(keys[i], vals[i]);
}
}
keys = t.keys;
vals = t.vals;
m = t.m;
}
public static void main(String[] args)
{
LinearProbingHashST<String, String> st = new LinearProbingHashST<String, String>();
String[] data = new String[]{"a", "b", "c", "d", "e", "f", "g", "h", "m"};
String[] val = new String[]{"aaa", "bbb", "ccc", "ddd", "eee", "fff", "ggg", "hhh", "mmm"};
for (int i = 0; i < data.length; i++)
{
st.put(data[i], val[i]);
}
for (int i = 0; i < data.length; i++)
{
System.out.println(data[i] + " " + st.get(data[i]));
}
}
}
3.R向單詞查找樹
3.1 定義
與各種查找樹一樣,單詞查找樹也是由鏈接的結點所組成的數據結構。每個結點只有一個父結點(根結點除外),每個結點都含有R條鏈接,其中R爲字母表的大小。每個鍵所關聯的值保存在該鍵的最後一個字母所對應的結點中。值爲空的結點在符號表中沒有對應的鍵,它們的存在是爲了簡化單詞查找樹中的查找操作。
3.2 查找操作
單詞查找樹的查找操作非常簡單,從首字母開始延着樹結點查找就可以:
- 鍵的尾字符所對應的結點中的值非空,命中!
- 鍵的尾字符所對應的結點中的值爲空,未命中!
- 查找結束於一條空鏈接,未命中!
3.3 插入操作
和二叉查找樹一樣,在插入之前要進行一次查找。
在到達鍵的尾字符之前就遇到了一個空鏈接。證明不存在匹配的結點,爲鍵中還未被檢查的每個字符創建一個對應的結點,並將鍵對應的值保存到最後一個字符的結點中。
在遇到空鏈接之前就到達了鍵的尾字符。將該結點的值設爲鍵對應的值(無論該值是否爲空)。
3.4 刪除操作
刪除的第一步是找到鍵所對應的結點並將它的值設爲空null. 如果該結點含有一個非空的鏈接指向某個子結點,那麼就不需要再進行其他操作了。如果它的所有鏈接均爲空,那就需要從數據結構中刪除這個結點。如果刪除它使得它的父結點的所有鏈接也均爲空,就要繼續刪除它的父結點,依此類推。
3.5 代碼實現
//基於R向單詞查找樹的符號表
public class TrieST<Value> {
private static int R = 256; //基數
private Node root;
private static class Node
{
private Object val;
private Node[] next = new Node[R];
}
@SuppressWarnings("unchecked")
public Value get(String key)
{
Node x = get(root, key, 0);
if(x == null)
{
return null;
}
return (Value)x.val;
}
private Node get(Node x, String key, int d)
{
//返回以x作爲根結點的字單詞查找樹中與key相關聯的值
if(x == null)
{
return null;
}
if(d == key.length())
{
return x;
}
char c = key.charAt(d);//找到第d個字符所對應的字單詞查找樹
return get(x.next[c], key, d + 1);
}
public void put(String key, Value val)
{
root = put(root, key, val, 0);
}
private Node put(Node x, String key, Value val, int d)
{
//如果key存在於以x爲根結點的子單詞查找樹中則更新與它相關聯的值
if(x == null)
{
x = new Node();
}
if(d == key.length())
{
x.val = val;
return x;
}
char c = key.charAt(d);//找到第d個字符所對應的字單詞查找樹
x.next[c] = put(x.next[c], key, val, d + 1);
return x;
}
public void delete(String key)
{
root = delete(root, key, 0);
}
private Node delete(Node x, String key, int d)
{
if(x == null)
{
return null;
}
if(d == key.length())
{
x.val = null;
}
else
{
char c= key.charAt(d);
x.next[c] = delete(x.next[c], key, d+1);
}
if(x.val != null)
{
return x;
}
for(char c = 0; c < R; c++)
{
if(x.next[c] != null)
{
return x;
}
}
return null;
}
public static void main(String[] args)
{
TrieST<Integer> newST = new TrieST<Integer>();
String[] keys= {"Nicholas", "Nate", "Jenny", "Penny", "Cynthina", "Michael"};
for(int i = 0; i < keys.length; i++)
{
newST.put(keys[i], i);
}
newST.delete("Penny");
for(int i = 0; i < keys.length; i++)
{
Object val = newST.get(keys[i]);
System.out.println(keys[i] + " " + val);
}
}
}
4.三向單詞查找樹
4.1 定義
三向單詞查找樹可以避免R向單詞查找樹過度的空間消耗。它的每個結點都含有一個字符、三條鏈接和一個值。三條鏈接分別對應當前字母小於、等於和大於結點字母的所有鍵。
4.1 查找、插入和刪除操作
在查找時,首先比較鍵的首字母和根結點的字母。如果鍵的首字母較小,就選擇左鏈接;如果較大,就選擇右鏈接;如果相等則選擇中鏈接。然後遞歸地使用相同的算法。如果遇到一個空鏈接或者當鍵結束時結點的值爲空,那麼查找未命中。如果鍵結束時結點的值非空則查找命中。
插入一個新鍵時,首先進行查找,然後和單詞查找樹一樣,在樹中補全鍵末尾的所有結點。
在三向單詞查找樹中,需要使用在二叉查找樹中刪除結點的方法來刪去與該字符對應的結點。
4.3 代碼實現
//基於三向單詞查找樹的符號表
public class TST<Value> {
private Node root;
private class Node
{
char c;
Node left, mid, right;
Value val;
}
public Value get(String key)
{
Node x = get(root, key, 0);
if(x == null)
{
return null;
}
return x.val;
}
private Node get(Node x, String key, int d)
{
if(x == null)
{
return null;
}
char c = key.charAt(d);
if(c < x.c)
{
return get(x.left, key, d);
}
else if(c > x.c)
{
return get(x.right, key, d);
}
else if(d < key.length() - 1)
{
return get(x.mid, key, d + 1);
}
else
{
return x;
}
}
public void put(String key, Value val)
{
root = put(root, key, val, 0);
}
private Node put(Node x, String key, Value val, int d)
{
char c = key.charAt(d);
if(x == null)
{
x = new Node();
x.c = c;
}
if(c < x.c)
{
x.left = put(x.left, key, val, d);
}
else if(c > x.c)
{
x.right = put(x.right, key, val, d);
}
else if(d < key.length() - 1)
{
x.mid = put(x.mid, key, val, d + 1);
}
else
{
x.val = val;
}
return x;
}
public static void main(String[] args)
{
TST<Integer> newTST = new TST<Integer>();
String[] keys= {"Nicholas", "Nate", "Jenny", "Penny", "Cynthina", "Michael"};
for(int i = 0; i < keys.length; i++)
{
newTST.put(keys[i], i);
}
for(int i = 0; i < keys.length; i++)
{
int val = newTST.get(keys[i]);
System.out.println(keys[i] + " " + val);
}
}
}
它的每個結點只含有三個鏈接,因此所需空間遠小於對應的單詞查找樹。使用三向單詞查找樹的最大好處是它能夠很好地適應實際應用中可能出現的被查找鍵的不規則性。它可以使用256個字符的ASCII編碼或者65536個字符的Unicode編碼,而不必擔心分支帶來的巨大開銷。