集合(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; } } }