Data Structure_二叉樹_集合_堆_并查集_哈希表

前情提要——二叉樹

二叉樹之前已經(jīng)提到過毙替,二叉樹這種數(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)下面拿出來的樹都可以作為一個(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è)元素也很簡單肮砾,查看當(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

前中后序遍歷都很簡單,代碼和前面C++都都是一樣的懒闷。對(duì)于中序遍歷的非遞歸遍歷十减。非遞歸遍歷可以對(duì)比遞歸來實(shí)現(xiàn)栈幸,數(shù)據(jù)結(jié)構(gòu)里面有遞歸屬性的只有棧了,所以可以用棧來實(shí)現(xiàn)帮辟。先把根元素壓進(jìn)棧速址,由于前序遍歷直接輸出第一個(gè)遍歷到到元素,所以先出棧輸出之后再把出棧的元素的子節(jié)點(diǎn)壓進(jìn)去由驹,要注意的是右孩子先壓芍锚,左孩子再壓,因?yàn)楸闅v的順序是先遍歷左邊再遍歷右邊蔓榄,以此類推并炮,只到空為止。
遞歸處理很簡單甥郑,幾行就好了逃魄,主要繁瑣到就是非遞歸遍歷到過程,前序遍歷的非遞歸澜搅。這個(gè)算算比較簡單到伍俘,因?yàn)橄缺闅v到是一開始遍歷到到點(diǎn),再依次是左右子樹勉躺,沒有倒敘過程癌瘾,都是有順序性到,所以可以直接用棧處理饵溅,先把跟節(jié)點(diǎn)扔進(jìn)去柳弄,如果棧不為空,那么就要出棧概说,直接輸出當(dāng)前元素碧注,在放入左右子樹即可,但是放入左右子樹需要注意糖赔,應(yīng)當(dāng)先放入右子樹再放入左子樹萍丐,因?yàn)槭窍缺闅v左子樹再遍歷右子樹,而棧是反過來的放典。

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)該是遍歷一開始要拿出來輸出都元素弥臼,所以可以先把左邊子樹都遍歷完存到棧里面宴咧,然后以這些存到棧里面的元素為起點(diǎn)遍歷下去。每次出來一個(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è)問題搏明,這里的右子樹是先輸出再到當(dāng)前節(jié)點(diǎn)的,首先要拿到當(dāng)前節(jié)點(diǎn)闪檬,然后再看看右子樹有沒有熏瞄,有就遍歷,等右子樹遍歷完之后當(dāng)前節(jié)點(diǎn)還在棧里面谬以,這個(gè)時(shí)候再拿出來的還是當(dāng)前節(jié)點(diǎn)强饮,這個(gè)時(shí)候就不知道右子樹有沒有被遍歷過了,這就進(jìn)入到了一個(gè)死循環(huán)为黎,所以這里要使用一個(gè)標(biāo)記來看看有沒有訪問了右子樹邮丰,如果訪問了右子樹,就可以放心輸出了铭乾,因?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)說過妇押,直接給出代碼即可:

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)上去柴我,替代原來的即可。

    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ì)可以用來做去重工作。再上面實(shí)現(xiàn)的二分搜索樹是不可以存放重復(fù)數(shù)據(jù)的彤悔,所以可以作為集合的一種底層實(shí)現(xiàn)方式。二叉樹的實(shí)現(xiàn)是有要求的索守,要求一定要是二叉樹的結(jié)構(gòu)來實(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)用很廣蚓峦,訪問量的統(tǒng)計(jì)舌剂,詞匯量的統(tǒng)計(jì)等等都可以用到集合去重。首先是基于BST的集合暑椰,上面實(shí)現(xiàn)的BST完全包含了集合的功能霍转。直接包裝一下即可。

Leecode804

Leecode804一汽,摩斯碼的判斷:



這個(gè)題目非常簡單避消,直接替換一下扔到集合里面就好了,這里使用的就是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è)等等,是屬于有序集合枫虏,而鏈表這些就屬于無序性的了妇穴。

優(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++有寫過死讹,對(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ù)雜度就是2log(n)〕甑剩可以直接用新的元素替換最大值惠况,然后做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ì)列就很簡單了完箩,出隊(duì)列的時(shí)候就直接extract最大值即可。

優(yōu)先隊(duì)列347


給定一個(gè)數(shù)組拉队,數(shù)組元素可以重復(fù)弊知,那么數(shù)字就會(huì)出現(xiàn)重復(fù)這個(gè)問題,在這種情況下粱快,就需要求出前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));
    }

}

并查集

之前包括前面博客所討論的樹問題都是樹問題漫雷,這些樹問題都是由父親指向孩子,而這個(gè)并查集是孩子指向樹鳍咱,可以由孩子找到跟節(jié)點(diǎ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ù)雜度是很快的宝惰,但是合并就很慢了,需要O(n)的復(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)類似鏈表那樣長長的數(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)該反著過來,所以應(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)被壓縮成了長條型贱勃,就需要使用路徑壓縮來優(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è)簡單的問題:


返回第一個(gè)不重復(fù)的字符贵扰。最簡單的處理方式就是使用一個(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è)問題就隱藏著哈希表這種數(shù)據(jù)結(jié)構(gòu)耘子。上面的frequency數(shù)組就是一個(gè)哈希表,每一個(gè)字符都和一個(gè)索引對(duì)應(yīng)球切。數(shù)組的查找是支持下表操作的拴还,所有復(fù)雜度可以是O(1)的復(fù)雜度。哈希其實(shí)就是使用一個(gè)下標(biāo)來指示一個(gè)數(shù)值或者是字符欧聘,然后解決哈希沖突片林。簡單的來說,哈希就體現(xiàn)了用空間換時(shí)間的思想。
鍵通過哈希函數(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)成整型處理

在Java里面的HashCode是以整型為基準(zhǔn)的探橱,他只是給出了hashcode,索引下標(biāo)還是需要其他的計(jì)算绘证。

hash沖突——鏈地址法

如果沒有沖突隧膏,那么就按照下標(biāo)直接存放,如果產(chǎn)生了沖突嚷那,也就是一個(gè)索引下有兩個(gè)相同的哈希值胞枕,那么就用鏈表把他們都串起來,如果還是有相同的那么繼續(xù)接上魏宽,所以每一個(gè)空間等于是存上了一個(gè)鏈表腐泻,也就是一個(gè)查找表,既然是查找表那么久不一定是鏈表了湖员,可以是樹結(jié)構(gòu)贫悄,紅黑樹二叉樹都可以。在Java8之前娘摔,一直都是一個(gè)位置對(duì)應(yīng)一個(gè)鏈表窄坦,Java8開始如果沖突達(dá)到了一定程度,也就是鏈表里面元素過多了凳寺,那么就會(huì)把每一個(gè)位置自動(dòng)轉(zhuǎn)成紅黑樹鸭津。因?yàn)槿绻跀?shù)據(jù)量少的情況下,使用鏈表查找是更加快的肠缨,因?yàn)闆]有紅黑樹的維護(hù)過程逆趋,而數(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ù)鍵值取哈希值然后求余名斟。過程很簡單。如果插入的數(shù)據(jù)是N個(gè)魄眉,哈希表大小是M砰盐,如果每一個(gè)位置是鏈表的話,那么平均復(fù)雜度就是O(N/M)坑律,如果是平衡樹就是O(log(N/M))岩梳。可以看到這個(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ù)過少那么久可以縮榮膘魄。
首先就是要實(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中用的是原來的M,而遍歷的時(shí)候要遍歷的是原來的M创葡,所以應(yīng)該要保存一份浙踢。之后就只需要在添加和移除兩個(gè)操作修改容量即可。
由于均攤復(fù)雜度是由O(N/M)決定的灿渴,所以復(fù)雜度是在O(lowerTol)-O(upperTol)洛波。但事實(shí)上這樣擴(kuò)容還有一個(gè)問題,乘上兩倍之后M就不是素?cái)?shù)了骚露,所以動(dòng)態(tài)擴(kuò)容的時(shí)候還需要選取素?cái)?shù)的問題蹬挤。
哈希表的均攤復(fù)雜度那么久接近于O(1),很快棘幸,但是得到了什么就會(huì)失去了什么焰扳,這里哈希表久犧牲了順序性,樹結(jié)構(gòu)都是有順序性的误续,所以稍微慢一點(diǎn)吨悍。哈希表其實(shí)也是一個(gè)集合,有序集合可以用樹結(jié)構(gòu)作為底層數(shù)據(jù)結(jié)構(gòu)來實(shí)現(xiàn)蹋嵌,無序集合可以用哈希表來實(shí)現(xiàn)育瓜。

hash沖突——開放地址法

如果遇到了沖突,那么就用線性探測的方法栽烂,加一看看有沒有空的躏仇,沒有繼續(xù)加一恋脚。但是這樣效率有時(shí)候是很低的,這里就可以采用類似計(jì)算機(jī)網(wǎng)絡(luò)里面的碰撞檢測方法焰手,平方探測慧起,一開始是1,又沖突了就4册倒,9蛇耀,16這樣既可延刘。另外還可以使用二次哈希的方法。

最后附上所有GitHub代碼:https://github.com/GreenArrow2017/DataStructure_Java

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市攒砖,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌江掩,老刑警劉巖庄吼,帶你破解...
    沈念sama閱讀 219,110評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異域慷,居然都是意外死亡荒辕,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,443評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門犹褒,熙熙樓的掌柜王于貴愁眉苦臉地迎上來抵窒,“玉大人,你說我怎么就攤上這事叠骑±罨剩” “怎么了?”我有些...
    開封第一講書人閱讀 165,474評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵宙枷,是天一觀的道長掉房。 經(jīng)常有香客問我,道長慰丛,這世上最難降的妖魔是什么卓囚? 我笑而不...
    開封第一講書人閱讀 58,881評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮诅病,結(jié)果婚禮上哪亿,老公的妹妹穿的比我還像新娘。我一直安慰自己睬隶,他們只是感情好锣夹,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,902評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著苏潜,像睡著了一般银萍。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上恤左,一...
    開封第一講書人閱讀 51,698評(píng)論 1 305
  • 那天贴唇,我揣著相機(jī)與錄音搀绣,去河邊找鬼。 笑死戳气,一個(gè)胖子當(dāng)著我的面吹牛链患,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播瓶您,決...
    沈念sama閱讀 40,418評(píng)論 3 419
  • 文/蒼蘭香墨 我猛地睜開眼麻捻,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼!你這毒婦竟也來了呀袱?” 一聲冷哼從身側(cè)響起贸毕,我...
    開封第一講書人閱讀 39,332評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎夜赵,沒想到半個(gè)月后明棍,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,796評(píng)論 1 316
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡寇僧,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,968評(píng)論 3 337
  • 正文 我和宋清朗相戀三年摊腋,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片嘁傀。...
    茶點(diǎn)故事閱讀 40,110評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡兴蒸,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出心包,到底是詐尸還是另有隱情类咧,我是刑警寧澤馒铃,帶...
    沈念sama閱讀 35,792評(píng)論 5 346
  • 正文 年R本政府宣布蟹腾,位于F島的核電站,受9級(jí)特大地震影響区宇,放射性物質(zhì)發(fā)生泄漏娃殖。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,455評(píng)論 3 331
  • 文/蒙蒙 一议谷、第九天 我趴在偏房一處隱蔽的房頂上張望炉爆。 院中可真熱鬧,春花似錦卧晓、人聲如沸芬首。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,003評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽郁稍。三九已至,卻和暖如春胜宇,著一層夾襖步出監(jiān)牢的瞬間耀怜,已是汗流浹背恢着。 一陣腳步聲響...
    開封第一講書人閱讀 33,130評(píng)論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留财破,地道東北人掰派。 一個(gè)月前我還...
    沈念sama閱讀 48,348評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像左痢,于是被迫代替她去往敵國和親靡羡。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,047評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • 上一篇文章講述了樹的概念俊性, 特征以及分類亿眠, 旨在讓我們理解什么是樹, 樹的一些常用的概念是什么磅废,樹的分類有哪些等纳像。...
    DevCW閱讀 2,028評(píng)論 4 10
  • 堆 堆這種數(shù)據(jù)結(jié)構(gòu)的應(yīng)用很廣泛,比較常用的就是優(yōu)先隊(duì)列拯勉。普通的隊(duì)列就是先進(jìn)先出竟趾,后進(jìn)后出。優(yōu)先隊(duì)列就不太一樣宫峦,出隊(duì)...
    冒綠光的盒子閱讀 492評(píng)論 0 3
  • 樹的概述 樹是一種非常常用的數(shù)據(jù)結(jié)構(gòu)岔帽,樹與前面介紹的線性表,棧导绷,隊(duì)列等線性結(jié)構(gòu)不同犀勒,樹是一種非線性結(jié)構(gòu) 1.樹的定...
    Jack921閱讀 4,457評(píng)論 1 31
  • 在項(xiàng)目Code Review的時(shí)候,遇到了獲取Long對(duì)象的兩種方式: new Long() Long.value...
    zlup閱讀 8,190評(píng)論 0 0
  • 友誼的三角形 突然想起小學(xué)時(shí)候的我們仨妥曲,有著長長大黑辮子贾费,大大黑眼睛的小c,很瘦很瘦又暴脾氣的小l檐盟,還有一個(gè)總是團(tuán)...
    KK_485d閱讀 140評(píng)論 0 0