集合(Set)的基礎概念:java
本小節演示一下如何基於二分搜索樹實現一個集合,咱們都知道二分搜索樹一般不存放重複元素,且不採用中序遍歷的狀況下訪問元素是「無序」的(但一般基於樹實現的集合是有序集合),正好符合集合的特性,能夠直接做爲集合的底層實現。node
首先,咱們要實現一個簡單的二分搜索樹。具體代碼以下:編程
package tree; import java.util.LinkedList; import java.util.Queue; import java.util.Stack; /** * 二分搜索樹 * 因爲存儲的數據需具備可比較性,因此泛型需繼承Comparable接口 * * @author 01 **/ public class BinarySearchTree<E extends Comparable<E>> { /** * 節點結構 */ private class Node { E e; Node left; Node right; public Node() { this(null, null, null); } public Node(E e) { this(e, null, null); } public Node(E e, Node left, Node right) { this.e = e; this.left = left; this.right = right; } } /** * 根節點 */ private Node root; /** * 表示樹裏存儲的元素個數 */ private int size; /** * 獲取樹裏的元素個數 * * @return 元素個數 */ public int size() { return size; } /** * 樹是否爲空 * * @return 爲空返回true,不然返回false */ public boolean isEmpty() { return size == 0; } /** * 向二分搜索樹中添加一個新元素e * * @param e 新元素 */ public void add(E e) { if (root == null) { // 根節點爲空的處理 root = new Node(e); size++; } else { add(root, e); } } /** * 向以node爲根的二分搜索樹中插入元素e,遞歸實現 */ private void add(Node node, E e) { // 遞歸的終止條件 if (e.equals(node.e)) { // 不存儲重複元素 return; } else if (e.compareTo(node.e) < 0 && node.left == null) { // 元素e小於node節點的元素,而且node節點的左孩子爲空,因此成爲node節點的左孩子 node.left = new Node(e); size++; return; } else if (e.compareTo(node.e) > 0 && node.right == null) { // 元素e大於node節點的元素,而且node節點的右孩子爲空,因此成爲node節點的右孩子 node.right = new Node(e); size++; return; } if (e.compareTo(node.e) < 0) { // 元素e小於node節點的元素,往左子樹走 add(node.left, e); } else { // 元素e大於node節點的元素,往右子樹走 add(node.right, e); } } /** * 從二分搜索樹中刪除元素爲e的節點 */ public void remove(E e) { root = remove(root, e); } /** * 刪除以node爲根的二分搜索樹中值爲e的節點,遞歸實現 * 返回刪除節點後新的二分搜索樹的根 */ private Node remove(Node node, E e) { if (node == null) { return null; } 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; } // 找到了要刪除的節點 // 待刪除的節點左子樹爲空的狀況 if (node.left == null) { // 若是有右子樹,須要將其掛到被刪除的節點上 Node rightNode = node.right; node.right = null; size--; return rightNode; } // 待刪除的節點右子樹爲空的狀況 if (node.right == null) { // 若是有左子樹,須要將其掛到被刪除的節點上 Node leftNode = node.left; node.left = null; size--; return leftNode; } // 待刪除的節點左右子樹均不爲空的狀況 // 找到比待刪除節點大的最小節點,即待刪除節點右子樹的最小節點 Node successor = minimum(node.right); // 用這個節點替換待刪除節點的位置 // 因爲removeMin裏已經維護過一次size了,因此這裏就不須要維護一次了 successor.right = removeMin(node.right); successor.left = node.left; return successor; } /** * 查看二分搜索樹中是否包含元素e */ public boolean contains(E e) { return contains(root, e); } /** * 查看以node爲根節點的二分搜索樹中是否包含元素e,遞歸實現 */ private boolean contains(Node node, E e) { if (node == null) { return false; } if (e.compareTo(node.e) == 0) { return true; } else if (e.compareTo(node.e) < 0) { // 找左子樹 return contains(node.left, e); } // 找右子樹 return contains(node.right, e); } }
有了二分搜索樹這個底層數據結構以後,實現集合就很簡單了,由於二分搜索樹基本能夠覆蓋集合的特性。因爲集合是一個相對上層的數據結構,因此在實現集合時須要定義一個接口,抽象出集合的操做。這樣底層不管使用什麼數據結構實現,對於上層來講都是無感知的,這也是面向接口編程的好處。接口定義以下:數組
package set; /** * 集合接口 * * @author 01 * @date 2021-01-18 **/ public interface Set<E> { /** * 添加元素 * * @param e e */ void add(E e); /** * 刪除元素 * * @param e e */ void remove(E e); /** * 是否包含指定元素 * * @param e e * @return boolean */ boolean contains(E e); /** * 獲取集合中的元素個數 * * @return int */ int getSize(); /** * 集合是否爲空 * * @return boolean */ boolean isEmpty(); }
在集合接口的具體實現類中,基本只須要調用二分搜索樹的方法便可,這樣咱們很簡單就實現了一個集合數據結構。代碼以下:數據結構
package set; import tree.BinarySearchTree; /** * 基於二分搜索樹實現的集合 * * @author 01 * @date 2021-01-18 **/ public class TreeSet<E extends Comparable<E>> implements Set<E> { private final BinarySearchTree<E> bst; public TreeSet() { bst = new BinarySearchTree<>(); } @Override public void add(E e) { bst.add(e); } @Override public void remove(E e) { bst.remove(e); } @Override public boolean contains(E e) { return bst.contains(e); } @Override public int getSize() { return bst.size(); } @Override public boolean isEmpty() { return bst.isEmpty(); } }
使用其餘數據結構,例如鏈表也能實現集合,同爲線性結構的動態數組也能夠。本小節簡單演示下,基於基於鏈表的集合實現。和以前同樣,首先實現一個簡單的鏈表數據結構,代碼以下:編程語言
package linkedlist; /** * 單向鏈表數據結構 * * @author 01 * @date 2018-11-08 **/ public class LinkedList<E> { /** * 鏈表中的節點 */ private class Node { E e; Node next; public Node() { this(null, null); } public Node(E e) { this(e, null); } public Node(E e, Node next) { this.e = e; this.next = next; } @Override public String toString() { return e.toString(); } } /** * 虛擬頭節點 */ private Node dummyHead; /** * 鏈表中元素的個數 */ private int size; public LinkedList() { this.dummyHead = new Node(null, null); this.size = 0; } /** * 獲取鏈表中的元素個數 * * @return 元素個數 */ public int getSize() { return size; } /** * 鏈表是否爲空 * * @return 爲空返回true,不然返回false */ public boolean isEmpty() { return size == 0; } /** * 在鏈表的index(0-based)位置添加新的元素e * * @param index 元素添加的位置 * @param e 新的元素 */ public void add(int index, E e) { if (index < 0 || index > size) { throw new IllegalArgumentException("Add failed. Illegal index."); } Node prev = dummyHead; // 移動prev到index前一個節點的位置 for (int i = 0; i < index; i++) { prev = prev.next; } Node node = new Node(e); node.next = prev.next; prev.next = node; // 一樣,以上三句代碼能夠一句代碼完成 // prev.next = new Node(e, prev.next); size++; } /** * 在鏈表頭添加新的元素e * * @param e 新的元素 */ public void addFirst(E e) { add(0, e); } /** * 查找鏈表中是否包含元素e */ public boolean contains(E e) { Node cur = dummyHead.next; // 第一種遍歷鏈表的方式 while (cur != null) { if (cur.e.equals(e)) { return true; } cur = cur.next; } return false; } /** * 從鏈表中刪除元素e */ public void removeElement(E e) { Node prev = dummyHead; while (prev.next != null) { if (prev.next.e.equals(e)) { break; } prev = prev.next; } if (prev.next != null) { Node delNode = prev.next; prev.next = delNode.next; delNode.next = null; size--; } } }
而後基於這個鏈表結構就能夠輕易實現集合了。代碼以下:ide
package set; import linkedlist.LinkedList; /** * 基於鏈表實現的集合 * * @author 01 * @date 2021-01-18 **/ public class LinkedListSet<E> implements Set<E> { private final LinkedList<E> linkedList; public LinkedListSet() { linkedList = new LinkedList<>(); } @Override public void add(E e) { // 不存儲重複元素 if (!linkedList.contains(e)) { linkedList.addFirst(e); } } @Override public void remove(E e) { linkedList.removeElement(e); } @Override public boolean contains(E e) { return linkedList.contains(e); } @Override public int getSize() { return linkedList.getSize(); } @Override public boolean isEmpty() { return linkedList.isEmpty(); } }
映射(Map)在數據結構中是指一種key-value的數據結構,key與value是有具備一對一關係的,因此稱之爲映射。這與數學中的映射概念同樣,定義域與值域具備一對一的映射關係,描述這個映射關係的是函數:
函數
映射這個詞相對來講有些晦澀,咱們也能夠將其類比成字典,這也是爲何一些編程語言中將其稱爲字典(一般縮寫爲dict)的緣由。由於字典就是一種典型的映射關係,一個詞對應着一個釋義,也是key-value的結構,經過key咱們就能快速找到value。大數據
其實映射在咱們的平常生活中無處不在,例如,身份證 -> 人、車牌號 -> 車以及工牌 -> 員工等。因此Map在不少領域都有着很重要的做用,最典型的就是大數據領域中的核心思想:Map-Reduce,典型的應用就是詞頻統計:單詞 -> 頻率。this
與集合同樣,映射也是一個相對上層的數據結構,底層也能夠由多種不一樣的數據結構來實現,常見的底層實現有:鏈表、二分搜索樹、紅黑樹以及哈希表等。因此咱們須要定義一個Map接口做爲上層抽像:
package map; /** * 映射接口 * * @author 01 * @date 2021-01-18 **/ public interface Map<K, V> { /** * 添加元素 * * @param key 鍵 * @param value 值 */ void add(K key, V value); /** * 根據key刪除元素 * * @param key 鍵 * @return 被刪除的value */ V remove(K key); /** * 根據key查詢元素是否存在 * * @param key key * @return boolean */ boolean contains(K key); /** * 根據key獲取value * * @param key key * @return value */ V get(K key); /** * 改變key的value * * @param key key * @param value value */ void set(K key, V value); /** * 獲取Map中的元素個數 * * @return 元素個數 */ int getSize(); /** * 判斷Map是否爲空 * * @return boolean */ boolean isEmpty(); }
使用鏈表來實現映射,與實現普通的鏈表差異不大,惟一不一樣的就是鏈表中的節點再也不是簡單地存儲單個元素,而是須要有兩個成員變量分別存儲key和value。具體的實現代碼以下:
package map; /** * 基於鏈表實現的Map * * @author 01 * @date 2021-01-18 */ public class LinkedListMap<K, V> implements Map<K, V> { /** * 鏈表的節點結構,節點中會存儲鍵值對,而不是單個元素 */ private class Node { public K key; public V value; public Node next; public Node(K key, V value, Node next) { this.key = key; this.value = value; this.next = next; } public Node(K key, V value) { this(key, value, null); } public Node() { this(null, null, null); } @Override public String toString() { return key.toString() + " : " + value.toString(); } } /** * 虛擬頭節點 */ private final Node dummyHead; private int size; public LinkedListMap() { dummyHead = new Node(); size = 0; } /** * 根據傳入的key獲取鏈表中的節點 */ private Node getNode(K key) { Node cur = dummyHead.next; while (cur != null) { if (cur.key.equals(key)) { return cur; } cur = cur.next; } return null; } @Override public int getSize() { return size; } @Override public boolean isEmpty() { return size == 0; } @Override public boolean contains(K key) { return getNode(key) != null; } @Override public V get(K key) { Node node = getNode(key); return node == null ? null : node.value; } @Override public void add(K key, V value) { Node node = getNode(key); if (node == null) { // key不存在,往鏈表的頭部插入新元素 dummyHead.next = new Node(key, value, dummyHead.next); size++; } else { // 不然,改變value node.value = value; } } @Override public void set(K key, V newValue) { Node node = getNode(key); if (node == null) { throw new IllegalArgumentException(key + " doesn't exist!"); } node.value = newValue; } @Override public V remove(K key) { Node prev = dummyHead; // 根據key找到待刪除節點的前一個節點 while (prev.next != null) { if (prev.next.key.equals(key)) { break; } prev = prev.next; } if (prev.next != null) { // 刪除目標節點 Node delNode = prev.next; prev.next = delNode.next; delNode.next = null; size--; return delNode.value; } return null; } }
最後,咱們來看一下基於二分搜索樹的映射實現。看了以前基於鏈表的實現案例後,對本小節的內容就很容易理解了,由於基於二分搜索樹的映射實現也是同樣的,除了樹的節點結構不同外,其他的邏輯與普通的二分搜索樹沒啥太大區別。具體實現代碼以下:
package map; /** * 基於二分搜索樹實現的Map * * @author 01 * @date 2021-01-18 */ public class TreeMap<K extends Comparable<K>, V> implements Map<K, V> { /** * 二分搜索樹的節點結構,節點中會存儲鍵值對,而不是單個元素 */ private class Node { public K key; public V value; public Node left, right; public Node(K key, V value) { this.key = key; this.value = value; left = null; right = null; } } private Node root; private int size; public TreeMap() { root = null; size = 0; } @Override public int getSize() { return size; } @Override public boolean isEmpty() { return size == 0; } @Override public void add(K key, V value) { // 向二分搜索樹中添加新的元素(key, value) root = add(root, key, value); } /** * 向以node爲根的二分搜索樹中插入元素(key, value),遞歸實現 * * @return 返回插入新節點後二分搜索樹的根 */ private Node add(Node node, K key, V value) { if (node == null) { size++; return new Node(key, value); } if (key.compareTo(node.key) < 0) { node.left = add(node.left, key, value); } else if (key.compareTo(node.key) > 0) { node.right = add(node.right, key, value); } else { node.value = value; } return node; } /** * 返回以node爲根節點的二分搜索樹中,key所在的節點 */ private Node getNode(Node node, K key) { if (node == null) { return null; } if (key.equals(node.key)) { return node; } else if (key.compareTo(node.key) < 0) { return getNode(node.left, key); } else { return getNode(node.right, key); } } @Override public boolean contains(K key) { return getNode(root, key) != null; } @Override public V get(K key) { Node node = getNode(root, key); return node == null ? null : node.value; } @Override public void set(K key, V newValue) { Node node = getNode(root, key); if (node == null) { throw new IllegalArgumentException(key + " doesn't exist!"); } node.value = newValue; } /** * 返回以node爲根的二分搜索樹的最小值所在的節點 */ private Node minimum(Node node) { if (node.left == null) { return node; } return minimum(node.left); } /** * 刪除掉以node爲根的二分搜索樹中的最小節點 * 返回刪除節點後新的二分搜索樹的根 */ private Node removeMin(Node node) { if (node.left == null) { Node rightNode = node.right; node.right = null; size--; return rightNode; } node.left = removeMin(node.left); return node; } @Override public V remove(K key) { Node node = getNode(root, key); if (node != null) { // 從二分搜索樹中刪除鍵爲key的節點 root = remove(root, key); return node.value; } return null; } private Node remove(Node node, K key) { if (node == null) { return null; } if (key.compareTo(node.key) < 0) { node.left = remove(node.left, key); return node; } else if (key.compareTo(node.key) > 0) { node.right = remove(node.right, key); return node; } else { // 待刪除節點左子樹爲空的狀況 if (node.left == null) { Node rightNode = node.right; node.right = null; size--; return rightNode; } // 待刪除節點右子樹爲空的狀況 if (node.right == null) { Node leftNode = node.left; node.left = null; size--; return leftNode; } // 待刪除節點左右子樹均不爲空的狀況 // 找到比待刪除節點大的最小節點,即待刪除節點右子樹的最小節點 Node successor = minimum(node.right); // 用這個節點頂替待刪除節點的位置 successor.right = removeMin(node.right); successor.left = node.left; return successor; } } }