前情提要——二叉樹
二叉樹之前已經(jīng)提到過(guò),二叉樹這種數(shù)據(jù)結(jié)構(gòu)只能有兩個(gè)子數(shù),一左一右。
葉子節(jié)點(diǎn)就是左右孩子都是空的,但是并不是每一顆樹都像上圖所示的那樣這么規(guī)整,有些樹樹可以只有左孩子沒有右孩子的。二叉樹的節(jié)點(diǎn)一定會(huì)大于左節(jié)點(diǎn)的值小于右節(jié)點(diǎn)的值,每一個(gè)節(jié)點(diǎn)都要滿足,所有每一個(gè)節(jié)點(diǎn)下面拿出來(lái)的樹都可以作為一個(gè)二叉樹。既然有大于等于了,那么這科樹的元素一定要有可比較性才可以。
Create a BST
package Tree.BST;
public class BST<E extends Comparable<E>> {
/**
* Binary Search Tree
*/
private class Node {
public E e;
public Node left, right;
public Node(E e) {
this.e = e;
left = right = null;
}
}
private Node root;
private int size;
public BST() {
root = null;
size = 0;
}
public int size(){
return size;
}
public boolean isEmpty(){
return size == 0;
}
}
和上面描述的基本一致的。
Insert an element
插入一個(gè)元素也很簡(jiǎn)單,查看當(dāng)前元素是否是大于或者小于節(jié)點(diǎn)元素,如果是大于,那么就右邊遞歸即可,二叉樹的插入非遞歸寫法和鏈表很像。
public void add(E e) {
root = add(root, e);
}
private Node add(Node node, E e) {
if (node == null) {
size++;
return new Node(e);
}
if (e.compareTo(node.e) < 0) {
node.left = add(node.left, e);
} else {
node.right = add(node.right, e);
}
return node;
}
Select an element
判斷一個(gè)元素和查找一個(gè)元素算法基本一致,小于左邊找,大于右邊找即可。
···
public boolean contains(E e) {
return contains(root, e);
}
public boolean contains(Node node, E e) {
if (node == null) {
return false;
} else if (e.equals(node.e)) {
return true;
} else if (e.compareTo(node.e) < 0) {
return contains(node.left, e);
} else {
return contains(node.right, e);
}
}
···
Traversal
前中后序遍歷都很簡(jiǎn)單,代碼和前面C++都都是一樣的。對(duì)于中序遍歷的非遞歸遍歷。非遞歸遍歷可以對(duì)比遞歸來(lái)實(shí)現(xiàn),數(shù)據(jù)結(jié)構(gòu)里面有遞歸屬性的只有棧了,所以可以用棧來(lái)實(shí)現(xiàn)。先把根元素壓進(jìn)棧,由于前序遍歷直接輸出第一個(gè)遍歷到到元素,所以先出棧輸出之后再把出棧的元素的子節(jié)點(diǎn)壓進(jìn)去,要注意的是右孩子先壓,左孩子再壓,因?yàn)楸闅v的順序是先遍歷左邊再遍歷右邊,以此類推,只到空為止。
遞歸處理很簡(jiǎn)單,幾行就好了,主要繁瑣到就是非遞歸遍歷到過(guò)程,前序遍歷的非遞歸。這個(gè)算算比較簡(jiǎn)單到,因?yàn)橄缺闅v到是一開始遍歷到到點(diǎn),再依次是左右子樹,沒有倒敘過(guò)程,都是有順序性到,所以可以直接用棧處理,先把跟節(jié)點(diǎn)扔進(jìn)去,如果棧不為空,那么就要出棧,直接輸出當(dāng)前元素,在放入左右子樹即可,但是放入左右子樹需要注意,應(yīng)當(dāng)先放入右子樹再放入左子樹,因?yàn)槭窍缺闅v左子樹再遍歷右子樹,而棧是反過(guò)來(lái)的。
public void preOrderNonRecur() {
Stack<Node> stack = new Stack<>();
stack.push(root);
while (!stack.isEmpty()) {
Node node = stack.pop();
System.out.print(node.e + " ");
if (node.right != null)
stack.push(node.right);
if (node.left != null)
stack.push(node.left);
}
System.out.println();
}
這就是前序遍歷。中序的非遞歸遍歷就有點(diǎn)復(fù)雜了,中序遍歷是左中右,這個(gè)時(shí)候順序就不是都往下了,沒有辦法一次性就遍歷完,棧里面一開始存儲(chǔ)都應(yīng)該是遍歷一開始要拿出來(lái)輸出都元素,所以可以先把左邊子樹都遍歷完存到棧里面,然后以這些存到棧里面的元素為起點(diǎn)遍歷下去。每次出來(lái)一個(gè)元素都要看看這個(gè)元素的右子樹是否存在,如果存在就要遍歷,但其實(shí)不必要這樣判斷,因?yàn)樗惴ㄇ懊娴拇笱h(huán)已經(jīng)判斷了。
public void inOrderNonRecur() {
Stack<Node> stack = new Stack<>();
Node node = root;
while (node != null || !stack.isEmpty()) {
while (node != null) {
stack.push(node);
node = node.left;
}
if (!stack.isEmpty()) {
Node node1 = stack.pop();
System.out.print(node1.e + " ");
node = node1.right;
}
}
System.out.println();
對(duì)于后序遍歷就是最復(fù)雜的了,由于后序遍歷和前序遍歷都是逆的,所以一開始也要先把左子樹放到棧里面,出的時(shí)候在看看有沒有右子樹。但是這里有個(gè)問(wèn)題,這里的右子樹是先輸出再到當(dāng)前節(jié)點(diǎn)的,首先要拿到當(dāng)前節(jié)點(diǎn),然后再看看右子樹有沒有,有就遍歷,等右子樹遍歷完之后當(dāng)前節(jié)點(diǎn)還在棧里面,這個(gè)時(shí)候再拿出來(lái)的還是當(dāng)前節(jié)點(diǎn),這個(gè)時(shí)候就不知道右子樹有沒有被遍歷過(guò)了,這就進(jìn)入到了一個(gè)死循環(huán),所以這里要使用一個(gè)標(biāo)記來(lái)看看有沒有訪問(wèn)了右子樹,如果訪問(wèn)了右子樹,就可以放心輸出了,因?yàn)閣hile循環(huán)的時(shí)候已經(jīng)到了最左邊的了,這個(gè)時(shí)候不會(huì)再有左子樹,只需要考慮右子樹即可。
public void lastOrderNonRecur() {
Stack<Node> stack = new Stack<>();
Node node = root;
while (node != null || !stack.isEmpty()) {
while (node != null) {
stack.push(node);
node = node.left;
}
if (!stack.isEmpty()) {
Node node1 = stack.peek();
if (!node1.isright) {
node1.isright = true;
node = node1.right;
} else {
node = stack.pop();
System.out.print(node.e + " ");
node = null;
}
}
}
System.out.println();
}
中序遍歷和后序遍歷都從左邊擴(kuò)散到右邊。
最后一個(gè)就是層序遍歷,層序遍歷就是廣度優(yōu)先遍歷,實(shí)現(xiàn)用隊(duì)列就好了,事實(shí)上大多數(shù)的廣度優(yōu)先遍歷基本都是要使用隊(duì)列的。之前的數(shù)據(jù)結(jié)構(gòu)說(shuō)過(guò),直接給出代碼即可:
levelOrder(){
Queue<Node> nodes = new LinkedList<>();
nodes.add(root);
while (!nodes.isEmpty()){
Node node = nodes.remove();
System.out.print(node.e + " ");
if (node.left != null){
nodes.add(node.left);
}
if (node.right != null){
nodes.add(node.right);
}
}
System.out.println();
}
remove an specific element
刪除一個(gè)元素有點(diǎn)麻煩,如果只有一邊有元素,那么就只需要把一邊的移動(dòng)上去替代即可,如果兩邊都有子樹,那么就需要把右子樹最小的一個(gè)移動(dòng)上去,當(dāng)然,其實(shí)也可以把左子樹最大的移動(dòng)上去,替代原來(lái)的即可。
private Node remove(Node node, E e) {
if (node == null) {
return null;
} else if (e.compareTo(node.e) < 0) {
node.left = remove(node.left, e);
return node;
} else if (e.compareTo(node.e) > 0) {
node.right = remove(node.right, e);
return node;
} else{
if (node.left == null){
Node node1 = node.right;
node.right = null;
size--;
return node1;
}else if (node.right == null){
Node node1 = node.left;
node.left = null;
size--;
return node1;
}else {
Node successor = new Node(minimum(node.right).e);
successor.left = node.left;
successor.right = removeMin(node.right);
node.left = node.right = null;
return successor;
}
}
}
集合Set
集合這種數(shù)據(jù)結(jié)構(gòu)有點(diǎn)像高中數(shù)學(xué)那種集合,集合有一個(gè)特點(diǎn),就是每一個(gè)元素只能有一個(gè),這個(gè)性質(zhì)可以用來(lái)做去重工作。再上面實(shí)現(xiàn)的二分搜索樹是不可以存放重復(fù)數(shù)據(jù)的,所以可以作為集合的一種底層實(shí)現(xiàn)方式。二叉樹的實(shí)現(xiàn)是有要求的,要求一定要是二叉樹的結(jié)構(gòu)來(lái)實(shí)現(xiàn),而集合只是要求有這么多功能就OK,所以集合屬于一種高級(jí)數(shù)據(jù)結(jié)構(gòu),沒有具體實(shí)現(xiàn)方法。
集合基本方法
Set創(chuàng)建一個(gè)集合,remove移除集合的一個(gè)元素,contains查看集合是否包含這個(gè)元素,getSize獲得集合大小,isEmpty查看集合是否為空,add添加一個(gè)元素,不能添加重復(fù)的元素。
集合的應(yīng)用很廣,訪問(wèn)量的統(tǒng)計(jì),詞匯量的統(tǒng)計(jì)等等都可以用到集合去重。首先是基于BST的集合,上面實(shí)現(xiàn)的BST完全包含了集合的功能。直接包裝一下即可。
Leecode804
Leecode804,摩斯碼的判斷:

這個(gè)題目非常簡(jiǎn)單,直接替換一下扔到集合里面就好了,這里使用的就是treeSet,是使用的紅黑樹實(shí)現(xiàn)方式:
class Solution {
public int uniqueMorseRepresentations(String[] words) {
String [] codes = {".-","-...","-.-.","-..",".","..-.","--.","....","..",".---","-.-",".-..","--","-.","---",".--.","--.-",".-.","...","-","..-","...-",".--","-..-","-.--","--.."};
TreeSet<String> set = new TreeSet<>();
for (String word : words){
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < word.length(); i++) {
stringBuilder.append(codes[word.charAt(i) - 'a']);
}
set.add(stringBuilder.toString());
}
return set.size();
}
}
之前所實(shí)現(xiàn)的集合,二分搜索樹,紅黑樹這些實(shí)現(xiàn)的集合都是有順序性的,因?yàn)檫@些結(jié)構(gòu)實(shí)現(xiàn)的集合是很容易可以排序(中序遍歷),找到下一個(gè)上一個(gè)等等,是屬于有序集合,而鏈表這些就屬于無(wú)序性的了。
優(yōu)先隊(duì)列和堆
堆的基本結(jié)構(gòu), 這里的堆實(shí)現(xiàn)也和樹有關(guān)系,二叉堆。二叉堆是一個(gè)完全二叉樹。
package Heap;
import Array.Array;
public class MaxHeap<E extends Comparable<E>> {
private Array<E> data;
public MaxHeap(int capacity) {
data = new Array<E>(capacity);
}
public MaxHeap() {
data = new Array<E>();
}
public int size() {
return data.getSize();
}
public boolean isEmpty() {
return data.isEmpty();
}
private int parent(int index) {
if (index == 0) {
throw new IllegalArgumentException("No parents when index equal zero!");
}
return (index - 1) / 2;
}
private int leftChild(int index) {
return index * 2 + 1;
}
private int rightChild(int index) {
return index * 2 + 2;
}
}
堆的實(shí)現(xiàn)之前的C++有寫過(guò),對(duì)于插入一個(gè)元素,插入在數(shù)組的最后面,然后再按照規(guī)則慢慢的shiftUp上去,如果這個(gè)元素是大于它的父母,那么就要浮上去,然后以父母為當(dāng)前元素繼續(xù)循環(huán)判斷:
private void shiftUp(int index) {
while (index > 0 && data.get(index).compareTo(data.get(parent(index))) > 0) {
data.swap(index, parent(index));
index = parent(index);
}
}
輸出最大值的元素就只需要把第一個(gè)元素輸出,把最后一個(gè)元素補(bǔ)上,再把新的第一個(gè)元素進(jìn)行shiftDown操作:
private void shiftDown(int index){
while (leftChild(index) < data.getSize()){
int j = leftChild(index);
if (j + 1 < data.getSize() && data.get(j + 1).compareTo(data.get(j)) > 0){
j = rightChild(index);
}
if (data.get(index).compareTo(data.get(j)) >= 0){
break;
}
data.swap(index, j);
index = j;
}
}
堆還有一個(gè)replace操作,取出最大值,用另一個(gè)值替代,可以先輸出最大值,然后再添加另一個(gè)值,但是這樣這樣復(fù)雜度就是。可以直接用新的元素替換最大值,然后做shiftDown操作即可。
public E replace(E e) {
E max = data.get(0);
data.set(0, e);
shiftDown(0);
return max;
}
如果存在一個(gè)數(shù)組,想要直接在數(shù)組上操作,使得這個(gè)數(shù)組直接變成一個(gè)堆,那就需要heapify操作了。從最后一個(gè)葉子節(jié)點(diǎn)開始不斷做shiftdown操作即可:
for (int i = parent(this.data.getSize() - 1); i >= 0; i --){
shiftDown(i);
}
基于堆的優(yōu)先隊(duì)列就很簡(jiǎn)單了,出隊(duì)列的時(shí)候就直接extract最大值即可。
優(yōu)先隊(duì)列347

給定一個(gè)數(shù)組,數(shù)組元素可以重復(fù),那么數(shù)字就會(huì)出現(xiàn)重復(fù)這個(gè)問(wèn)題,在這種情況下,就需要求出前N個(gè)頻率最高的元素,這就有點(diǎn)像優(yōu)先隊(duì)列了。先用Treemap把頻率存儲(chǔ)到樹里面,然后放到優(yōu)先隊(duì)列里面輸出即可:
package Queue.Leecode347;
import java.util.LinkedList;
import java.util.List;
import java.util.PriorityQueue;
import java.util.TreeMap;
class Solution {
private class Freq implements Comparable<Freq>{
int e, freq;
public Freq(int e, int freq){
this.e = e;
this.freq = freq;
}
@Override
public int compareTo(Freq another) {
if (this.freq < another.freq){
return -1;
}
else if (this.freq > another.freq){
return 1;
}else {
return 0;
}
}
}
public List<Integer> topKFrequent(int[] nums, int k) {
TreeMap<Integer, Integer> map = new TreeMap<>();
for (int n : nums) {
if (map.containsKey(n)) {
map.put(n, map.get(n) + 1);
} else {
map.put(n, 1);
}
}
PriorityQueue<Freq> priorityQueue = new PriorityQueue<>();
for(int key: map.keySet()){
if(priorityQueue.size() < k)
priorityQueue.add(new Freq(key, map.get(key)));
else if(map.get(key) > priorityQueue.peek().freq){
priorityQueue.remove();
priorityQueue.add(new Freq(key, map.get(key)));
}
}
List<Integer> linkedList = new LinkedList<>();
while (!priorityQueue.isEmpty()){
linkedList.add(priorityQueue.remove().e);
}
return linkedList;
}
public static void main(String[] args) {
int[] nums = {1,1,1,2,2,3};
Solution solution = new Solution();
System.out.println(solution.topKFrequent(nums, 2));
}
}
并查集
之前包括前面博客所討論的樹問(wèn)題都是樹問(wèn)題,這些樹問(wèn)題都是由父親指向孩子,而這個(gè)并查集是孩子指向樹,可以由孩子找到跟節(jié)點(diǎn)。并查集可以用來(lái)回答連接問(wèn)題。給出兩個(gè)點(diǎn),看看這兩個(gè)點(diǎn)是否是連接的。前面博客提到的就是找到根然后比較兩個(gè)根是不是一樣的即可。這里的并查集主要實(shí)現(xiàn)兩個(gè)操作,Union和isConnected,連接兩個(gè)節(jié)點(diǎn)和查看兩個(gè)節(jié)點(diǎn)是否是連接的。
并查集Quick Find
每個(gè)元素的下標(biāo)都會(huì)有一個(gè)標(biāo)記,如果標(biāo)記相同那么就是同一個(gè)類別,也就是連接在了一起。一開始每一個(gè)數(shù)字就是一個(gè)類別,所以一開始下標(biāo)都是不一樣的。
private int find(int p) {
if (p < 0 || p >= parent.length) {
throw new IllegalArgumentException("p is out of range!");
}
while (p != parent[p]) {
p = parent[p];
}
return p;
}
@Override
public int getSize() {
return parent.length;
}
@Override
public boolean isConnected(int p, int q) {
return find(p) == find(q);
}
@Override
public void unionElements(int p, int q) {
int pRoot = find(p);
int qRoot = find(q);
if (pRoot == qRoot){
return;
}
parent[pRoot] = qRoot;
}
這種情況下的的查找復(fù)雜度是很快的,但是合并就很慢了,需要的復(fù)雜度。
基于樹的Quick union
每一個(gè)節(jié)點(diǎn)都指向上一個(gè)節(jié)點(diǎn),最后指向的就是根,根的parent就是他自己,如果根相同,那么這兩個(gè)節(jié)點(diǎn)就是相同的。
private int find(int p) {
if (p < 0 || p >= parent.length) {
throw new IllegalArgumentException("p is out of range!");
}
while (p != parent[p]) {
p = parent[p];
}
return p;
}
@Override
public int getSize() {
return parent.length;
}
@Override
public boolean isConnected(int p, int q) {
return find(p) == find(q);
}
@Override
public void unionElements(int p, int q) {
int pRoot = find(p);
int qRoot = find(q);
if (pRoot == qRoot){
return;
}
parent[pRoot] = qRoot;
}
合并的時(shí)候直接把根節(jié)點(diǎn)合并就好了。但是這種合并有個(gè)弊端,如果合并的時(shí)候恰好把大的哪一組數(shù)據(jù)合并到了小的哪一組數(shù)據(jù)上,就容易出現(xiàn)類似鏈表那樣長(zhǎng)長(zhǎng)的數(shù)據(jù)段,這個(gè)時(shí)候就可以先做判斷,看看哪一邊的數(shù)據(jù)量大就把小數(shù)據(jù)的合并到大的一邊去即可。所以就需要一個(gè)記錄數(shù)量的數(shù)組。
@Override
public void unionElements(int p, int q) {
int pRoot = find(p);
int qRoot = find(q);
if (pRoot == qRoot) {
return;
}
if (sz[pRoot] < sz[qRoot]) {
parent[pRoot] = qRoot;
sz[qRoot] += sz[pRoot];
}else {
parent[qRoot] = pRoot;
sz[pRoot] += sz[qRoot];
}
}
其他都是一樣的。這樣其實(shí)不太嚴(yán)謹(jǐn),如果數(shù)據(jù)都是分散開的,那么就應(yīng)該反著過(guò)來(lái),所以應(yīng)該以高度作為對(duì)比的條件:
@Override
public void unionElements(int p, int q) {
int pRoot = find(p);
int qRoot = find(q);
if (pRoot == qRoot) {
return;
}
if (rank[pRoot] < rank[qRoot]){
parent[pRoot] = qRoot;
}else if (rank[pRoot] > rank[qRoot]){
parent[qRoot] = pRoot;
}else {
parent[qRoot] = pRoot;
rank[pRoot] += 1;
}
路徑壓縮 Path Compression
如果這個(gè)并查集已經(jīng)被壓縮成了長(zhǎng)條型,就需要使用路徑壓縮來(lái)優(yōu)化了:
這種情況下就只需要指向父親的父親即可,只要加上一行代碼就可以。
private int find(int p) {
if (p < 0 || p >= parent.length) {
throw new IllegalArgumentException("p is out of range!");
}
while (p != parent[p]) {
parent[p] = parent[parent[p]];
p = parent[p];
}
return p;
}
哈希表HashTable
首先看一個(gè)簡(jiǎn)單的問(wèn)題:

返回第一個(gè)不重復(fù)的字符。最簡(jiǎn)單的處理方式就是使用一個(gè)映射,先計(jì)算一遍所有字符的頻率是多少,然后再遍歷一遍所有的字符,看看第一個(gè)字符出現(xiàn)次數(shù)為一是索引下標(biāo)是多少即可。
但是這樣比較麻煩,我們可以使用一個(gè)數(shù)組包含了二十六個(gè)字母。
class Solution {
public int firstUniqChar(String s) {
int[] freq = new int[26];
for (int i = 0; i < s.length(); i++) {
freq[s.charAt(i) - 'a']++;
}
for (int i = 0; i < s.length(); i++) {
if (freq[s.charAt(i) - 'a'] == 1) {
return i;
}
}
return -1;
}
}
在這個(gè)問(wèn)題就隱藏著哈希表這種數(shù)據(jù)結(jié)構(gòu)。上面的frequency數(shù)組就是一個(gè)哈希表,每一個(gè)字符都和一個(gè)索引對(duì)應(yīng)。數(shù)組的查找是支持下表操作的,所有復(fù)雜度可以是的復(fù)雜度。哈希其實(shí)就是使用一個(gè)下標(biāo)來(lái)指示一個(gè)數(shù)值或者是字符,然后解決哈希沖突。簡(jiǎn)單的來(lái)說(shuō),哈希就體現(xiàn)了用空間換時(shí)間的思想。
鍵通過(guò)哈希函數(shù)得到的索引分布越均勻越好。對(duì)于一些特殊的領(lǐng)域,有特殊領(lǐng)域的哈希函數(shù)設(shè)計(jì)方式甚至有專門的論文。
首先是整型哈希函數(shù)的設(shè)計(jì),小范圍整數(shù)直接使用,負(fù)整數(shù)就要進(jìn)行偏移。對(duì)于大整數(shù),就需要對(duì)這個(gè)大整數(shù)進(jìn)行處理,使得變成一個(gè)計(jì)算機(jī)可以處理的數(shù)據(jù)。常用的方法就是取模了。但是有時(shí)候取模使得分布不會(huì)有均勻的分布,所以可以使用素?cái)?shù)作為取模數(shù)值。
對(duì)于字符串的處理,就需要轉(zhuǎn)成整型處理

hash沖突——鏈地址法
如果沒有沖突,那么就按照下標(biāo)直接存放,如果產(chǎn)生了沖突,也就是一個(gè)索引下有兩個(gè)相同的哈希值,那么就用鏈表把他們都串起來(lái),如果還是有相同的那么繼續(xù)接上,所以每一個(gè)空間等于是存上了一個(gè)鏈表,也就是一個(gè)查找表,既然是查找表那么久不一定是鏈表了,可以是樹結(jié)構(gòu),紅黑樹二叉樹都可以。在Java8之前,一直都是一個(gè)位置對(duì)應(yīng)一個(gè)鏈表,Java8開始如果沖突達(dá)到了一定程度,也就是鏈表里面元素過(guò)多了,那么就會(huì)把每一個(gè)位置自動(dòng)轉(zhuǎn)成紅黑樹。因?yàn)槿绻跀?shù)據(jù)量少的情況下,使用鏈表查找是更加快的,因?yàn)闆]有紅黑樹的維護(hù)過(guò)程,而數(shù)據(jù)量多的時(shí)候就需要使用紅黑樹了。
public class Hash_Table<K, V> {
private TreeMap<K, V>[] hashtable;
private int M;
private int size;
public Hash_Table(int M) {
this.M = M;
size = 0;
hashtable = new TreeMap[M];
for (int i = 0; i < hashtable.length; i++) {
hashtable[i] = new TreeMap<>();
}
}
public Hash_Table() {
this(97);
}
private int hash(K key) {
return (key.hashCode() & 0x7fffffff) % M;
}
public int getSize() {
return size;
}
public void add(K key, V value) {
if (hashtable[hash(key)].containsKey(key)) {
hashtable[hash(key)].put(key, value);
} else {
hashtable[hash(key)].put(key, value);
size++;
}
}
public V remove(K key){
TreeMap<K, V> treeMap = hashtable[hash(key)];
V ret = null;
if (treeMap.containsKey(key)){
ret = treeMap.remove(key);
size--;
}
return ret;
}
public void set(K key, V value){
TreeMap<K, V> treeMap = hashtable[hash(key)];
if (!treeMap.containsKey(key)){
throw new IllegalArgumentException("no element!");
}else {
treeMap.put(key, value);
}
}
public boolean contains(K key){
return hashtable[hash(key)].containsKey(key);
}
public V get(K key){
return hashtable[hash(key)].get(key);
}
}
每一個(gè)位置都使用紅黑樹,插入的數(shù)據(jù)帶鍵值和數(shù)據(jù),根據(jù)鍵值取哈希值然后求余。過(guò)程很簡(jiǎn)單。如果插入的數(shù)據(jù)是N個(gè),哈希表大小是M,如果每一個(gè)位置是鏈表的話,那么平均復(fù)雜度就是,如果是平衡樹就是
。可以看到這個(gè)復(fù)雜度并沒有如期望的那樣,因?yàn)檫@是一個(gè)靜態(tài)數(shù)組,當(dāng)N大于M的時(shí)候那么就會(huì)趨向N了,復(fù)雜度就會(huì)回到鏈表的查找。所以可以考慮使用動(dòng)態(tài)數(shù)組的方法進(jìn)行擴(kuò)展。也就是讓M產(chǎn)生變化,當(dāng)N/M大于一定元素的時(shí)候就需要進(jìn)行擴(kuò)容,改變M了,當(dāng)插入數(shù)據(jù)過(guò)少那么久可以縮榮。
首先就是要實(shí)現(xiàn)一個(gè)resize方法了:
private void resize(int capacity){
TreeMap<K, V>[] newHashTable = new TreeMap[capacity];
for (int i = 0; i < capacity; i++) {
newHashTable[i] = new TreeMap<>();
}
int oldM = this.M;
this.M = capacity;
for (int i = 0; i < oldM; i++) {
TreeMap<K, V> treeMap = hashtable[i];
for (K key : treeMap.keySet()){
newHashTable[hash(key)].put(key, treeMap.get(key));
}
}
this.hashtable = newHashTable;
}
注意到M和新的M之間有一個(gè)陷阱,hash中用的是原來(lái)的M,而遍歷的時(shí)候要遍歷的是原來(lái)的M,所以應(yīng)該要保存一份。之后就只需要在添加和移除兩個(gè)操作修改容量即可。
由于均攤復(fù)雜度是由決定的,所以復(fù)雜度是在
。但事實(shí)上這樣擴(kuò)容還有一個(gè)問(wèn)題,乘上兩倍之后M就不是素?cái)?shù)了,所以動(dòng)態(tài)擴(kuò)容的時(shí)候還需要選取素?cái)?shù)的問(wèn)題。
哈希表的均攤復(fù)雜度那么久接近于,很快,但是得到了什么就會(huì)失去了什么,這里哈希表久犧牲了順序性,樹結(jié)構(gòu)都是有順序性的,所以稍微慢一點(diǎn)。哈希表其實(shí)也是一個(gè)集合,有序集合可以用樹結(jié)構(gòu)作為底層數(shù)據(jù)結(jié)構(gòu)來(lái)實(shí)現(xiàn),無(wú)序集合可以用哈希表來(lái)實(shí)現(xiàn)。
hash沖突——開放地址法
如果遇到了沖突,那么就用線性探測(cè)的方法,加一看看有沒有空的,沒有繼續(xù)加一。但是這樣效率有時(shí)候是很低的,這里就可以采用類似計(jì)算機(jī)網(wǎng)絡(luò)里面的碰撞檢測(cè)方法,平方探測(cè),一開始是1,又沖突了就4,9,16這樣既可。另外還可以使用二次哈希的方法。
最后附上所有GitHub代碼:https://github.com/GreenArrow2017/DataStructure_Java