本篇文章是为了记录自己在学习数据结构时的笔记,会对常见的数据结构做基本的介绍以及使用Java语言进行实现。包括
动态数组
栈
队列
链表
二分搜索树
优先队列和堆
线段树
Trie
并查集
AVL
树
红黑树
哈希表
动态数组 API介绍 数组是一种根据下标操作的数据结构,它的查询速度很快,但是它有缺点,那就是数组的容量一旦在创建时确定,就不能进行更改,所以为了克服这一缺点,我们实现一个自己的数组,并除此以外,还会实现一些方法,包括以下
add(int index, E e)
get(int index)
remove(int index)
set(int index, E e)
getSize()
contains(E e)
isEmpty()
find(E e)
返回数组中元素 e
第一次出现的 index
,若没有元素 e
,则返回 -1
新建一个 Array
类,它含有两个私有成员变量
除此以外还有两个构造方法
Array(int capacity)
Array()
public class Array <E > { private E[] data; private int size; public Array (int capacity) { data = (E[]) new Object[capacity]; size = 0 ; } public Array () { this (10 ); } }
现在我们来实现上面提到的方法。
方法实现 首先来实现 getSize()
方法,这个是返回数组元素的个数的,我们直接返回 size
即可
public int getSize () { return size; }
isEmpty()
是为了查看数组中是否还有元素,如果 size
为 0
的话说明数组为空,所以我们返回 size == 0
即可
public boolean isEmpty () { return size == 0 ; }
现在来实现 add(int index, E e)
方法,该方法的实现是将 index
后面的元素都向后移动一位,然后在 index
处插入元素 e
public void add (int index, E e) { if (index < 0 || index > size) { throw new IllegalArgumentException("参数错误" ); } for (int i = size; i > index; i--) { data[i] = data[i - 1 ]; } data[index] = e; size++; }
根据这个方法,我们可以很快的实现 addFirst(E e)
和 addLast(E e)
方法,这两个方法一个是在数组头添加元素,一个是在数组的末尾添加一个元素
public void addLast (E e) { add(size,e); } public void addFirst (E e) { add(0 ,e); }
下面来实现 remove(int index)
方法,该方法是删除 index
处的元素,并将该元素返回,以添加的操作相反,删除是将后面的元素向前移动,覆盖掉 index
处的元素即可删除
public E remove (int index) { if (index < 0 || index >= size) { throw new IllegalArgumentException("参数错误" ); } E e = data[index]; for (int i = index; i < size - 1 ; i++) { data[i] = data[i+1 ]; } size --; return e; }
同理,根据这个方法我们可以快速的实现 removeLast()
和 removeFirst()
方法
public E removeLast () { return remove(size -1 ); } public E removeFirst () { return remove(0 ); }
我们可以添加一个删除指定元素的方法 removeElement(E e)
,我们会遍历数组,如果发现有元素等于该元素,那么删除该元素并退出方法,所以这个方法只删除第一个元素 e
,并不是数组所有的元素 e
public void removeElement (E e) { for (int i = 0 ; i < size; i++) { if (e.equals(data[i])) { remove(i); return ; } } }
下面实现 contains(E e)
方法,这个方法的思路同删除指定元素相似,遍历数组,如果找到元素与指定元素相同,那么返回 true
,如果遍历完数组还没有找到与之相等的元素,那么返回 false
public boolean contains (E e) { for (int i = 0 ; i < size; i++) { if (data[i].equals(e)) { return true ; } } return false ; }
find(E e)
方法的实现也是遍历数组,如果找到了元素,那么返回下标,如果遍历完数组都没有找到,那么返回 -1
public int find (E e) { for (int i = 0 ; i < size; i++) { if (data[i].equals(e)) { return i; } } return -1 ; }
下面实现 get(int index)
和 set(int index, E e)
,这两个方法的实现及其简单,直接上代码
public E get (int index) { if (index < 0 || index >= size) { throw new IllegalArgumentException("参数错误" ); } return data[index]; } public void set (int index, E e) { if (index < 0 || index >= size) { throw new IllegalArgumentException("参数错误" ); } data[index] = e; }
我们可以根据 get
方法实现 getLast()
和 getFirst()
方法
public E getFirst () { return get(0 ); } public E getLast () { return get(size - 1 ); }
现在我们已经实现了 API
中提到的所有的方法,但是我们还是没有解决数组容量固定的问题,为了解决这个问题,我们需要实现一个 resize(int newCapacity)
,它的作用是改变数组的容量大小,这样当数组的容量不足时,我们调用该方法就可以将数组进行扩容,或者当数组中有大量空间空闲时,我们可以缩小数组的容量,代码如下
private void resize (int newCapacity) { E[] temp = (E[]) new Object[newCapacity]; for (int i =0 ; i < size; i++) { temp[i] = data[i]; } data = temp; }
现在我们改变 add(int index, E e)和remove(int index)
方法,我们会在添加元素和删除元素时检查数组的容量,以便对数组进行扩容或者缩容
public void add (int index, E e) { if (index < 0 || index > size) { throw new IllegalArgumentException("参数错误" ); } if (size == data.length) { resize(data.length * 2 ); } for (int i = size; i > index; i--) { data[i] = data[i - 1 ]; } data[index] = e; size++; }
public E remove (int index) { if (index < 0 || index >= size) { throw new IllegalArgumentException("参数错误" ); } E e = data[index]; for (int i = index; i < size - 1 ; i++) { data[i] = data[i+1 ]; } size --; if (size == data.length/4 ) { resize(data.length/2 ); } return e; }
为了方便的打印 Array
类,我们重写 toString()
方法如下
public String toString () { StringBuilder str = new StringBuilder(); str.append("size " + size); str.append(" capacity " + data.length); str.append("\n[" ); for (int i = 0 ; i < size; i++) { if (i == size - 1 ) { str.append(data[i].toString()); } else { str.append(data[i].toString() + ", " ); } } str.append("]" ); return str.toString(); }
至此,我们已经完全实现了 Array
,它的容量没有限制,并且提供了很多的方法供用户调用,我们将使用该类来实现其它的基本的数据结构。下面贴出完整的代码
public class Array <E > { private E[] data; private int size; public Array (int capacity) { data = (E[]) new Object[capacity]; size = 0 ; } public Array () { this (10 ); } public int getSize () { return size; } public boolean isEmpty () { return size == 0 ; } public void addLast (E e) { add(size,e); } public void addFirst (E e) { add(0 ,e); } public void add (int index, E e) { if (index < 0 || index > size) { throw new IllegalArgumentException("参数错误" ); } if (size == data.length) { resize(data.length * 2 ); } for (int i = size; i > index; i--) { data[i] = data[i - 1 ]; } data[index] = e; size++; } public E removeLast () { return remove(size -1 ); } public E removeFirst () { return remove(0 ); } public E remove (int index) { if (index < 0 || index >= size) { throw new IllegalArgumentException("参数错误" ); } E e = data[index]; for (int i = index; i < size - 1 ; i++) { data[i] = data[i+1 ]; } size --; if (size == data.length/4 ) { resize(data.length/2 ); } return e; } public void removeElement (E e) { for (int i = 0 ; i < size; i++) { if (e.equals(data[i])) { remove(i); return ; } } } public boolean contains (E e) { for (int i = 0 ; i < size; i++) { if (data[i].equals(e)) { return true ; } } return false ; } public int find (E e) { for (int i = 0 ; i < size; i++) { if (data[i].equals(e)) { return i; } } return -1 ; } private void resize (int newCapacity) { E[] temp = (E[]) new Object[newCapacity]; for (int i =0 ; i < size; i++) { temp[i] = data[i]; } data = temp; } public E get (int index) { if (index < 0 || index >= size) { throw new IllegalArgumentException("参数错误" ); } return data[index]; } public E getFirst () { return get(0 ); } public E getLast () { return get(size - 1 ); } public void set (int index, E e) { if (index < 0 || index >= size) { throw new IllegalArgumentException("参数错误" ); } data[index] = e; } public String toString () { StringBuilder str = new StringBuilder(); str.append("size " + size); str.append(" capacity " + data.length); str.append("\n[" ); for (int i = 0 ; i < size; i++) { if (i == size - 1 ) { str.append(data[i].toString()); } else { str.append(data[i].toString() + ", " ); } } str.append("]" ); return str.toString(); } }
栈 栈是一种先进后出的结构,比如你放书会把书放在最上面,最先放的书在最下面,而你拿书却是从最上面拿,最后放的最先拿到,栈正是怎么一种结构,我们规定最上面的位置叫做栈顶,我们向栈中添加元素是添加到栈顶,向栈中取出元素是从栈顶取出的,我们先来定义一个 Stack
接口,里面规定了一个栈包含的操作
public interface Stack <E > { void push (E e) ; E pop () ; boolean isEmpty () ; int getSize () ; E peek () ; }
下面我们将使用上面实现的 Array
来实现一个 ArrayStack
,我们把数组的最后位置定义为栈顶
public class ArrayStack <E > implements Stack <E > { private Array<E> data; public ArrayStack (int capacity) { data = new Array<>(capacity); } public ArrayStack () { data = new Array<>(); } @Override public void push (E e) { data.addLast(e); } @Override public E pop () { return data.removeLast(); } @Override public boolean isEmpty () { return data.isEmpty(); } @Override public int getSize () { return data.getSize(); } @Override public E peek () { return data.getLast(); } public String toString () { StringBuilder res = new StringBuilder(); res.append("Stack: " ); res.append("[" ); for (int i = 0 ; i < data.getSize(); i++) { res.append(data.get(i)); if (i != data.getSize()-1 ) { res.append(", " ); } } res.append("] top" ); return res.toString(); } }
上面的代码极其的简单,只要仔细的阅读就可以完全的理解,这里不多做解释。
下面介绍一个有关于栈的题目,此题来自于LeetCode第20题
给定一个只包括 '('
,')'
,'{'
,'}'
,'['
,']'
的字符串,判断字符串是否有效。有效字符串需满足:
左括号必须用相同类型的右括号闭合。
左括号必须以正确的顺序闭合。
注意空字符串可被认为是有效字符串。
这道题的解题思路是,如果遇到左括号'(', '[', '{'
,那么将左括号压入栈中,如果遇到右括号,那么将栈顶的左括号弹出,判断两个括号是否匹配,如果不匹配返回 fasle
,如果匹配进行下一轮,最后如果字符串遍历完毕,如果栈为空说明匹配成功,如果栈不为空,所以左边的括号多匹配失败,代码如下
import java.util.Stack;class Solution { public boolean isValid (String s) { Stack<Character> stack = new Stack<>(); for (int i = 0 ; i < s.length(); i++) { char c = s.charAt(i); if (c == '(' || c == '[' || c == '{' ) { stack.push(c); } else { if (stack.isEmpty()) { return false ; } char charTop = stack.pop(); if (c == ')' && charTop != '(' ) { return false ; } if (c == ']' && charTop != '[' ) { return false ; } if (c == '}' && charTop != '{' ) { return false ; } } } return stack.isEmpty(); } }
队列 队列是一种先进先出的结构,假设你在排队,那么最先排队的人最先得到服务。我们只能从队尾添加元素,从队首取出元素。老规矩,我们首先规定一下队列 Queue
的 API
public interface Queue <E > { void enqueue (E e) ; E dequeue () ; E getFront () ; int getSize () ; boolean isEmpty () ; }
数组队列 现在我们将使用动态数组 Array
类来实现队列,实现的逻辑也十分的简单,如下
public class ArrayQueue <E > implements Queue <E > { private Array<E> array; public ArrayQueue () { array = new Array<>(); } public ArrayQueue (int capacity) { array = new Array<>(capacity); } @Override public void enqueue (E e) { array.addLast(e); } @Override public E dequeue () { return array.removeFirst(); } @Override public E getFront () { return array.getFirst(); } @Override public int getSize () { return array.getSize(); } @Override public boolean isEmpty () { return array.isEmpty(); } @Override public String toString () { StringBuilder res = new StringBuilder(); res.append("Queue: " ); res.append("front [" ); for (int i = 0 ; i < array.getSize(); i++) { res.append(array.get(i)); if (i != array.getSize()-1 ) { res.append(", " ); } } res.append("] tail" ); return res.toString(); } }
注意上面我们的 dequeue
操作是调用了动态数组的 removeFirst
操作,这个操作需要遍历整个数组将元素向前移动,所以该操作是 O(n)
的。
循环队列 上面队列的 dequeue
操作是 O(n)
级别的,这是因为上面会将数组整体向前移一位,但是如果我们不这么做,而是增加一个变量 front
来记录队首的位置,这样我们只要将 front
向前移一位即可,这样的操作就是 O(1)
级别的
这样做的同时,我们发现,如果当 tail
来到数组的末尾,按道理应该将数组进行扩容,但是 front
前面还有空间
这个时候我们应当将 tail
移动到数组头去
这时 tail
的计算公式不再是简单的 tail = tail + 1
,而是 tail = (tail + 1) % data.length
,如果不理解这个式子,就想象一下时钟,11 点向前一步就是 12 点,也可以称为是 0 点,这个时候时钟的计算公式为 (11 + 1) % 12
。因为这种循环的特性,我们把这种实现方式称为循环队列。这次我们实现队列不在使用上面的动态数组,有了上面实现栈和队列的经验,想必可以容易理解下面的代码(在关键的步骤给予注释)
public class LoopQueue <E > implements Queue <E > { private int front; private int tail; private int size; private E[] data; public LoopQueue (int capacity) { data = (E[]) new Object[capacity]; size = 0 ; front = 0 ; tail = 0 ; } public LoopQueue () { this (10 ); } @Override public void enqueue (E e) { if (size == data.length) { resize(2 * data.length); } data[tail] = e; tail = (tail +1 ) % data.length; size++; } private void resize (int newCapacity) { E[] temp = (E[]) new Object[newCapacity]; for (int i =0 ; i < size; i++) { temp[i] = data[(front + i)%data.length]; } front = 0 ; tail = size; data = temp; } @Override public E dequeue () { if (size == 0 ) { throw new IllegalArgumentException("队列为空" ); } E e = data[front]; data[front] = null ; front = (front + 1 ) % data.length; size--; if (size == data.length / 4 ) { resize(data.length / 2 ); } return e; } @Override public E getFront () { if (size == 0 ) { throw new IllegalArgumentException("队列为空" ); } return data[front]; } @Override public int getSize () { return size; } @Override public boolean isEmpty () { return size == 0 ; } @Override public String toString () { StringBuilder str = new StringBuilder(); str.append("Queue: size " + size); str.append(" capacity " + data.length); str.append("\nfront [" ); for (int i = 0 ; i < size; i++) { if (i == size - 1 ) { str.append(data[(front + i) % data.length].toString()); } else { str.append(data[(front + i) % data.length].toString() + ", " ); } } str.append("] tail" ); return str.toString(); } }
这次我们得到的 dequeue
操作就是 O(1)
的了(严格的讲均摊复杂度为 O(1)
,因为里面 resize()
复杂度是 O(n)
的)。
链表 链表是一种非常重要的线性数据结构,我们在实现栈和队列时使用的是动态数组实现的,这个动态数组是针对用户而言是动态的,实际上底层是静态的,是通过 resize()
操作去解决容量问题的。而链表则是一种真正的动态数据结构,它是这么一种数据结构,我们把数据存储在一个节点(Node)中,一个节点一般包含两部分的内容,一个是存储的数据,一个是它要指向的下一个节点
class Node { private E e; private Node next; }
一个节点指向一个节点,所以最后看起来就像是一个链,我们把这种数据结构称为链表
最后一个节点的下一个节点为 `NULL`,表示后面没有节点了。它是一个真正的动态的数据结构,不需要处理容量的问题。但是它也有缺点,它没有数组那样快的查询能力,它要查询某个节点的数据,只能通过头结点一直寻找下来(后面我们将看到),所以它的查询速度比数组慢。
链表实现 现在我们将实现这么一个结构,首先设计好节点类
public class LinkedList <E > { private class Node <E > { public E e; public Node next; public Node (E e, Node next) { this .e = e; this .next = next; } public Node (E e) { this (e, null ); } public Node () { this (null , null ); } @Override public String toString () { return e.toString(); } } }
我们要想向链表中添加(或其他操作)元素,不可避免的要遍历链表(因为链表不能通过索引访问,只能通过前面的节点找到后面的节点),而要遍历链表,我们就要将链表的头存储起来,这样才能遍历链表,我们将链表的头称为 head
同时我们使用变量 `size` 来记录链表中元素的个数
public class LinkedList <E > { private Node head; private int size; public LinkedList () { head = null ; size = 0 ; } }
现在我们实现两个简单的方法 getSize()
和 isEmpty()
public int getSize () { return size; } public boolean isEmpty () { return size == 0 ; }
添加元素 向链表头添加元素
首先将要插入的新节点指向 `head`,然后将 `head` 设置为新节点,实现如下
public void addFirst (E e) { head = new Node(e,head); size++; }
在链表的中间添加一个元素
比如现在往节点 `1` 后面插入一个元素,首先将新节点指向节点 `2`,然后节点 `1` 指向新节点,实现如下
public void add (int index, E e) { if (index < 0 || index > size) { throw new IllegalArgumentException("参数错误" ); } if (index == 0 ) { addFirst(e); } Node prev = head; for (int i = 0 ; i < index - 1 ; i++) { prev = prev.next; } prev.next = new Node(e, prev.next); size++; }
向链表的尾部添加一个元素 直接复用上面的代码
public void addLast (E e) { add(size, e); }
虚拟头结点 我们在向链表中添加元素时,因为 head
前面没有节点,所以我们在添加元素时会对 head
进行单独的处理,为了不使 head
具有特殊性,我们在链表的最头部添加一个虚拟头结点,里面不存储元素,它的存在是为了使得操作链表方便
现在我们修改上面的 `head` 为 `dummyHead`
public class LinkedList <E > { private Node dummyHead; private int size; public LinkedList () { dummyHead = new Node(null , null ); size = 0 ; } public int getSize () { return size; } public boolean isEmpty () { return size == 0 ; } public void addFirst (E e) { add(0 ,e); } public void add (int index, E e) { if (index < 0 || index > size) { throw new IllegalArgumentException("参数错误" ); } Node prev = dummyHead; for (int i = 0 ; i < index; i++) { prev = prev.next; } prev.next = new Node(e, prev.next); size++; } public void addLast (E e) { add(size, e); } }
获得某个索引的值 实现的思路同 add
很像,不过这里我们找的不是前一个节点,而是当前的节点
public E get (int index) { if (index < 0 || index >= size) { throw new IllegalArgumentException("参数错误" ); } Node cur = dummyHead.next; for (int i = 0 ; i < index; i++) { cur = cur.next; } return (E) cur.e; }
基于这个方法,我们可以很快的实现 getFirst()
和 getLast()
public E getFirst () { return get(0 ); } public E getLast () { return get(size - 1 ); }
更新某个索引的值 实现的思路完全是同 get()
方法,直接上代码
public void set (int index, E e) { if (index < 0 || index >= size) { throw new IllegalArgumentException("参数错误" ); } Node cur = dummyHead.next; for (int i = 0 ; i < index; i++) { cur = cur.next; } cur.e = e; }
查找链表是否存在元素e public boolean contains (E e) { for (Node cur = dummyHead.next; cur != null ; cur = cur.next) { if (cur.e.equals(e)) { return true ; } } return false ; }
删除链表中的元素
上图已详细说明了操作的步骤,这里直接贴上代码实现
public E remove (int index) { if (index < 0 || index >= size) { throw new IllegalArgumentException("参数错误" ); } Node prev = dummyHead; for (int i = 0 ; i < index; i++) { prev = prev.next; } Node delNode = prev.next; prev.next = delNode.next; delNode.next = null ; size--; return (E) delNode.e; }
根据上面的方法,可以很快的实现 removeFirst()
和 removeLast()
方法
public E removeFirst () { return remove(0 ); } public E removeLast () { return remove(size - 1 ); }
toString() @Override public String toString () { StringBuilder res = new StringBuilder(); Node cur = dummyHead.next; while (cur != null ) { res.append(cur + "->" ); cur = cur.next; } res.append("NULL" ); return res.toString(); }
全部代码 public class LinkedList <E > { private class Node <E > { public E e; public Node next; public Node (E e, Node next) { this .e = e; this .next = next; } public Node (E e) { this (e, null ); } public Node () { this (null , null ); } @Override public String toString () { return e.toString(); } } private Node dummyHead; private int size; public LinkedList () { dummyHead = new Node(null , null ); size = 0 ; } public int getSize () { return size; } public boolean isEmpty () { return size == 0 ; } public void addFirst (E e) { add(0 ,e); } public void add (int index, E e) { if (index < 0 || index > size) { throw new IllegalArgumentException("参数错误" ); } Node prev = dummyHead; for (int i = 0 ; i < index; i++) { prev = prev.next; } prev.next = new Node(e, prev.next); size++; } public void addLast (E e) { add(size, e); } public E get (int index) { if (index < 0 || index >= size) { throw new IllegalArgumentException("参数错误" ); } Node cur = dummyHead.next; for (int i = 0 ; i < index; i++) { cur = cur.next; } return (E) cur.e; } public E getFirst () { return get(0 ); } public E getLast () { return get(size - 1 ); } public void set (int index, E e) { if (index < 0 || index >= size) { throw new IllegalArgumentException("参数错误" ); } Node cur = dummyHead.next; for (int i = 0 ; i < index; i++) { cur = cur.next; } cur.e = e; } public boolean contains (E e) { for (Node cur = dummyHead.next; cur != null ; cur = cur.next) { if (cur.e.equals(e)) { return true ; } } return false ; } public E remove (int index) { if (index < 0 || index >= size) { throw new IllegalArgumentException("参数错误" ); } Node prev = dummyHead; for (int i = 0 ; i < index; i++) { prev = prev.next; } Node delNode = prev.next; prev.next = delNode.next; delNode.next = null ; size--; return (E) delNode.e; } public E removeFirst () { return remove(0 ); } public E removeLast () { return remove(size - 1 ); } @Override public String toString () { StringBuilder res = new StringBuilder(); Node cur = dummyHead.next; while (cur != null ) { res.append(cur + "->" ); cur = cur.next; } res.append("NULL" ); return res.toString(); } }
使用链表实现栈 由于链表的 addFirst()
和 removeFirst()
的操作都是 O(1)
,所以我们使用链表头作为栈顶,具体的实现逻辑如下
public class LinkedListStack <E > implements Stack <E > { private LinkedList<E> linkedList; public LinkedListStack () { linkedList = new LinkedList<>(); } @Override public void push (E e) { linkedList.addFirst(e); } @Override public E pop () { return linkedList.removeFirst(); } @Override public boolean isEmpty () { return linkedList.isEmpty(); } @Override public int getSize () { return linkedList.getSize(); } @Override public E peek () { return linkedList.getFirst(); } @Override public String toString () { StringBuilder res = new StringBuilder(); res.append("Stack: top " ); res.append(linkedList); return res.toString(); } }
使用链表实现队列 我们之前使用数组实现队列,由于它的 dequeue
操作是 O(n)
级别的,所以我们使用 front
来标记队首,使用循环队列设计,同样的在链表中从链表尾部删除或增加元素都是 O(n)
级别的,为了解决这一个问题,我们决定在链表的尾部增加一个 tail
变量来标记,从而使得在尾部增加元素是 O(1)
级别的。
另外考虑在尾部删除一个元素是 O(1)
的吗? 答案是不是。因为我们删除一个节点需要知道该节点的前一个节点,而知道 tail
节点是无法知道 tail
的前一个节点的,我们还是要遍历。所以我们在 head
端删除元素,在 tail
端添加元素,并且由于只涉及到头部和尾部的操作,所以我们也不需要添加虚拟头结点了
下面就是实现的代码
public class LinkedListQueue <E > implements Queue <E > { private class Node <E > { public E e; public Node next; public Node (E e, Node next) { this .e = e; this .next = next; } public Node (E e) { this (e, null ); } public Node () { this (null , null ); } @Override public String toString () { return e.toString(); } } private Node head; private Node tail; private int size; public LinkedListQueue () { head = null ; tail = null ; size = 0 ; } @Override public void enqueue (E e) { if (size == 0 ) { tail = new Node(e); head = tail; } else { tail.next = new Node(e); tail = tail.next; } size++; } @Override public E dequeue () { if (size == 0 ) { throw new IllegalArgumentException("队列为空" ); } Node delNode = head; head = head.next; delNode.next = null ; size--; if (size == 0 ) { tail = null ; } return (E) delNode.e; } @Override public E getFront () { if (head == null ) { throw new IllegalArgumentException("队列为空" ); } return (E) head.e; } @Override public int getSize () { return size; } @Override public boolean isEmpty () { return size == 0 ; } @Override public String toString () { StringBuilder res = new StringBuilder(); res.append("Queue: front " ); Node cur = head; while (cur != null ) { res.append(cur + "->" ); cur = cur.next; } res.append("NULL tail" ); return res.toString(); } }
二分搜索树 什么是树结构
当你把上面的图倒过来看,就像是一棵树,所以我们把这种结构称为是树结构。那为什么要使用树结构,因为树结构在生活中很常见,如文件夹的组织方式,又或者如公司职能的组织方式,这些都是树结构的例子。为什么会使用树结构呢? 原因就是因为高效。
概念 同链表一样,它也是一种动态的数据结构,链表中的节点是指向一个节点,而二叉树是指向两个节点,我们把这两个节点称为左子树和右子树,又或者称为左孩子和右孩子。如下图表示的就是二叉树
class Node { E e; Node left; Node right; }
根节点
最顶部的那个节点,如上图中 28 就是根节点
二叉树具有唯一的一个根节点
叶子节点
二叉树的每个节点最多有两个孩子,最多有一个父亲
那是什么是二分搜索树,首先二分搜索树是二叉树,它满足这样的特点,对于每个节点
可以验算,上面的这棵树满足二分搜索树的性质,所以这棵树是二分搜索树。下面我们来实现二分搜索树中节点有关代码
public class BST <E extends Comparable <E >> { private class Node { public E e; public Node left; public Node right; public Node (E e) { this .e = e; left = null ; right = null ; } @Override public String toString () { return e.toString(); } } private Node root; private int size; public BST () { root = null ; size = 0 ; } public int size () { return size; } public boolean isEmpty () { return size == 0 ; } }
实现 添加元素
上图想必已经将添加元素的规则说的很详细了,所以这里直接上代码
public void add (E e) { if (root == null ) { root = new Node(e); size++; } else { add(root, e); } } private void add (Node node, E e) { if (e.equals(node.e)) { return ; } else if (e.compareTo(node.e) < 0 && node.left == null ) { node.left = new Node(e); size++; return ; } else if (e.compareTo(node.e) > 0 && node.right == null ) { node.right = new Node(e); size++; return ; } if (e.compareTo(node.e) < 0 ) { add(node.left, e); } if (e.compareTo(node.e) > 0 ) { add(node.right, e); } }
其实上面的代码还可以改进,因为我们在 add(E e)
中对 root
为根节点进行了单独的考虑,其实可以不再这里考虑,因为通过上面的规则知道,当一个节点为 null
时,不管它是根节点还是左右孩子,新加入的节点都将取代这个 null
节点
所以我们优化上面的代码如下
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 if (e.compareTo(node.e) > 0 ) { node.right = add(node.right,e); } return node; }
查询操作 public boolean contains (E e) { return contains(root, e); } private boolean contains (Node node, E e) { if (node == null ) { return false ; } 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); } }
二叉树的遍历 对于某个数据结构的遍历就是将该数据结构中所有的元素都访问一遍,分为三类
前序遍历 public void preOrder () { preOrder(root); } private void preOrder (Node node) { if (node == null ) { return ; } System.out.println(node); preOrder(node.left); preOrder(node.right); }
中序遍历 public void inOrder () { inOrder(root); } private void inOrder (Node node) { if (node == null ) { return ; } inOrder(node.left); System.out.println(node); inOrder(node.right); }
中序遍历的结果是元素从小到大排序。
后序遍历 public void postOrder () { postOrder(root); } private void postOrder (Node node) { if (node == null ) { return ; } postOrder(node.left); postOrder(node.right); System.out.println(node); }
后序遍历的一个应用是内存释放,我们必须先把左右孩子的内存释放完才能释放该节点的内存。
前序遍历的非递归实现
代码实现
public void preOrderNR () { if (root == null ) { return ; } Stack<Node> stack = new ArrayStack<>(); stack.push(root); while (!stack.isEmpty()) { Node top = stack.pop(); System.out.println(top); if (top.right != null ) { stack.push(top.right); } if (top.left != null ) { stack.push(top.left); } } }
层序遍历
代码实现
public void levelOrder () { if (root == null ) { return ; } Queue<Node> queue = new LoopQueue<>(); queue.enqueue(root); while (!queue.isEmpty()) { Node front = queue.dequeue(); System.out.println(front); if (front.left != null ) { queue.enqueue(front.left); } if (front.right != null ) { queue.enqueue(front.right); } } }
删除元素 在二分搜索树中删除一个节点是比较复杂的,我们首先从最简单的情况开始,删除二分搜索树中的最小值和最大值,首先是如何找到最大值和最小值
public E minimum () { if (size == 0 ) { throw new IllegalArgumentException("树为空" ); } return minimum(root).e; } private Node minimum (Node node) { if (node.left == null ) { return node; } return minimum(node.left); } public E maximum () { if (size == 0 ) { throw new IllegalArgumentException("树为空" ); } return maximum(root).e; } private Node maximum (Node node) { if (node.right == null ) { return node; } return maximum(node.right); }
找到了之后如何删除呢
public E removeMin () { E ret = minimum(); root = removeMin(root); return ret; } 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; }
public E removeMax () { E ret = maximum(); root = removeMax(root); return ret; } private Node removeMax (Node node) { if (node.right == null ) { Node leftNode = node.left; node.left = null ; size--; return leftNode; } node.right = removeMax(node.right); return node; }
现在讲解如何删除二分搜搜数中的任意一个节点
public void remove (E e) { root = remove(root, e); } private Node remove (Node node, E e) { if (node == null ) { return null ; } if (e.equals(node.e)) { if (node.right == null ) { Node leftNode = node.left; node.left = null ; size--; return leftNode; } else if (node.left == null ) { Node rightNode = node.right; node.right = null ; size--; return rightNode; } else { Node successor = minimum(node.right); successor.right = removeMin(node.right); successor.left = node.left; node.left = node.right = null ; return successor; } } else if (e.compareTo(node.e) < 0 ) { node.left = remove(node.left, e); } else { node.right = remove(node.right, e); } return node; }
上面说的是取右子树中的最小值,你也可以考虑取左子树中的最大值,道理都是一样的。
完整代码 public class BST <E extends Comparable <E >> { private class Node { public E e; public Node left; public Node right; public Node (E e) { this .e = e; left = null ; right = null ; } @Override public String toString () { return e.toString(); } } private Node root; private int size; public BST () { root = null ; size = 0 ; } public int size () { return size; } public boolean isEmpty () { return size == 0 ; } 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 if (e.compareTo(node.e) > 0 ) { node.right = add(node.right,e); } return node; } public boolean contains (E e) { return contains(root, e); } private boolean contains (Node node, E e) { if (node == null ) { return false ; } 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); } } public void preOrder () { preOrder(root); } private void preOrder (Node node) { if (node == null ) { return ; } System.out.println(node); preOrder(node.left); preOrder(node.right); } public void preOrderNR () { if (root == null ) { return ; } Stack<Node> stack = new ArrayStack<>(); stack.push(root); while (!stack.isEmpty()) { Node top = stack.pop(); System.out.println(top); if (top.right != null ) { stack.push(top.right); } if (top.left != null ) { stack.push(top.left); } } } public void inOrder () { inOrder(root); } private void inOrder (Node node) { if (node == null ) { return ; } inOrder(node.left); System.out.println(node); inOrder(node.right); } public void postOrder () { postOrder(root); } private void postOrder (Node node) { if (node == null ) { return ; } postOrder(node.left); postOrder(node.right); System.out.println(node); } public void levelOrder () { if (root == null ) { return ; } Queue<Node> queue = new LoopQueue<>(); queue.enqueue(root); while (!queue.isEmpty()) { Node front = queue.dequeue(); System.out.println(front); if (front.left != null ) { queue.enqueue(front.left); } if (front.right != null ) { queue.enqueue(front.right); } } } public E minimum () { if (size == 0 ) { throw new IllegalArgumentException("树为空" ); } return minimum(root).e; } private Node minimum (Node node) { if (node.left == null ) { return node; } return minimum(node.left); } public E maximum () { if (size == 0 ) { throw new IllegalArgumentException("树为空" ); } return maximum(root).e; } private Node maximum (Node node) { if (node.right == null ) { return node; } return maximum(node.right); } public E removeMin () { E ret = minimum(); root = removeMin(root); return ret; } 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; } public E removeMax () { E ret = maximum(); root = removeMax(root); return ret; } private Node removeMax (Node node) { if (node.right == null ) { Node leftNode = node.left; node.left = null ; size--; return leftNode; } node.right = removeMax(node.right); return node; } public void remove (E e) { root = remove(root, e); } private Node remove (Node node, E e) { if (node == null ) { return null ; } if (e.equals(node.e)) { if (node.right == null ) { Node leftNode = node.left; node.left = null ; size--; return leftNode; } else if (node.left == null ) { Node rightNode = node.right; node.right = null ; size--; return rightNode; } else { Node successor = minimum(node.right); successor.right = removeMin(node.right); successor.left = node.left; node.left = node.right = null ; return successor; } } else if (e.compareTo(node.e) < 0 ) { node.left = remove(node.left, e); } else { node.right = remove(node.right, e); } return node; } }
优先队列和堆 普通队列:先进先出,就像是我们在银行办业务或者是在超市买东西,但是考虑在医院,有病人有突发情况,这个时候容不得他去排队挂号了,这时他的优先级是比较高的,所以他需要得到优先的处理,像这种队列中的元素具有优先级的队列,我们把它称之为优先队列。在游戏中我们也会设置优先攻击血量最低的怪或者距离最近的怪,这时候血量和距离就成为了判断优先级的标准;在操作系统的任务调度,我们为程序分配 CPU,内存等等资源,并不是先到先得的,也是根据程序的优先级来进行分配的。
### 堆的结构
这里的堆指的是二叉堆,它满足以下的性质
- 堆中某个节点的值总是不大于其父亲节点的值(最大堆,相应也可以定义最小堆)
如果我们使用数组去实现堆
上面的序号表示的是在数组中的下标,我们发现如果父节点的下标为 `i`,那么左孩子的下标就为 `2i + 1`,右孩子的下标为 `2i + 2`,所以可以很快的根据父节点的下标得到左右孩子的下标,如果知道左右孩子的下标 `i`,那么 `(i - 1)/2` 就可以得到父节点的下标(整数除法,小数部分会被舍去)。这个结论可以使用数学归纳法进行证明,但不是这里的重点,所以不多做阐述。
public class MaxHeap <E extends Comparable <E >> { private Array<E> data; public MaxHeap (int capacity) { data = new Array<>(capacity); } public MaxHeap () { data = new Array<>(); } public int size () { return data.getSize(); } public boolean isEmpty () { return data.isEmpty(); } private int parent (int index) { return (index - 1 ) / 2 ; } private int leftChild (int index) { return 2 * index + 1 ; } private int rightChild (int index) { return 2 * index + 2 ; } }
堆的实现 向堆中添加元素
public void swap (int i, int j) { if (i < 0 || i >= size() || j < 0 || j >= size()) { throw new IllegalArgumentException("参数错误" ); } E temp = data.get(i); data.set(i, data.get(j)); data.set(j, temp); } public void add (E e) { data.addLast(e); siftUp(data.getSize() - 1 ); } private void siftUp (int index) { while (index != 0 && data.get(index).compareTo(data.get(parent(index))) > 0 ) { swap(index, parent(index)); index = parent(index); } }
向堆中取出最大元素
public E findMax () { if (isEmpty()) { throw new IllegalArgumentException("堆为空" ); } return data.get(0 ); } public E extractMax () { E ret = findMax(); swap(0 ,data.getSize() - 1 ); data.removeLast(); siftDown(0 ); return ret; } private void siftDown (int index) { while (leftChild(index) < size()) { int max = leftChild(index); int rightIndex = rightChild(index); if (rightIndex < size()) { max = data.get(max).compareTo(data.get(rightIndex)) > 0 ? max : rightIndex; } if (data.get(max).compareTo(data.get(index)) <= 0 ) { break ; } swap(max,index); index = max; } }
replace replace
操作指的是从堆中取出元素,并向堆中添加一个元素,实现的方法为
public E replace (E e) { E ret = findMax(); data.set(0 ,e); siftDown(0 ); return ret; }
heapify heapify
是指将任意一个数组整理成堆的形状,
我们把这个方法做成一个构造函数
public MaxHeap (E[] arr) { data = new Array<>(arr.length); for (int i = 0 ; i < arr.length; i++) { data.addLast(arr[i]); } for (int i = parent(data.getSize() -1 ); i >=0 ; i--) { siftDown(i); } }
完整代码 public class MaxHeap <E extends Comparable <E >> { private Array<E> data; public MaxHeap (int capacity) { data = new Array<>(capacity); } public MaxHeap () { data = new Array<>(); } public MaxHeap (E[] arr) { data = new Array<>(arr.length); for (int i = 0 ; i < arr.length; i++) { data.addLast(arr[i]); } for (int i = parent(data.getSize() -1 ); i >=0 ; i--) { siftDown(i); } } public int size () { return data.getSize(); } public boolean isEmpty () { return data.isEmpty(); } private int parent (int index) { return (index - 1 ) / 2 ; } private int leftChild (int index) { return 2 * index + 1 ; } private int rightChild (int index) { return 2 * index + 2 ; } public void swap (int i, int j) { if (i < 0 || i >= size() || j < 0 || j >= size()) { throw new IllegalArgumentException("参数错误" ); } E temp = data.get(i); data.set(i, data.get(j)); data.set(j, temp); } public void add (E e) { data.addLast(e); siftUp(data.getSize() - 1 ); } private void siftUp (int index) { while (index != 0 && data.get(index).compareTo(data.get(parent(index))) > 0 ) { swap(index, parent(index)); index = parent(index); } } public E findMax () { if (isEmpty()) { throw new IllegalArgumentException("堆为空" ); } return data.get(0 ); } public E extractMax () { E ret = findMax(); swap(0 ,data.getSize() - 1 ); data.removeLast(); siftDown(0 ); return ret; } private void siftDown (int index) { while (leftChild(index) < size()) { int max = leftChild(index); int rightIndex = rightChild(index); if (rightIndex < size()) { max = data.get(max).compareTo(data.get(rightIndex)) > 0 ? max : rightIndex; } if (data.get(max).compareTo(data.get(index)) <= 0 ) { break ; } swap(max,index); index = max; } } public E replace (E e) { E ret = findMax(); data.set(0 ,e); siftDown(0 ); return ret; } }
基于堆的优先队列 public class PriorityQueue <E extends Comparable <E >> implements Queue <E > { private MaxHeap<E> maxHeap; public PriorityQueue () { maxHeap = new MaxHeap<>(); } @Override public void enqueue (E e) { maxHeap.add(e); } @Override public E dequeue () { return maxHeap.extractMax(); } @Override public E getFront () { return maxHeap.findMax(); } @Override public int getSize () { return maxHeap.size(); } @Override public boolean isEmpty () { return maxHeap.isEmpty(); } }
线段树 对于有一类的问题,我们主要关心的是线段(区间),比如说查询一个区间 [i, j]
内的最大值,最小值等等。假设你有一个网站,你想查询某年(或某年以后)的用户访问量,消费最多的用户等等,这些都是在某个区间内进行查询,一般线段树的区间是固定的,不包含删除和添加的操作,只有查询和更新的操作
### 线段树的表示
现在如果假设有n个元素,用数组存储的话,需要多少空间呢
public class SegmentTree <E > { private E[] tree; private E[] data; public SegmentTree (E[] arr) { data = (E[]) new Object[arr.length]; for (int i = 0 ; i < arr.length; i++) { data[i] = arr[i]; } tree = (E[]) new Object[4 * data.length]; } public int getSize () { return data.length; } public E get (int index) { if (index < 0 || index >= data.length) { throw new IllegalArgumentException("参数错误" ); } return data[index]; } private int leftChild (int index) { return 2 * index + 1 ; } private int rightChild (int index) { return 2 * index + 2 ; } }
实现 创建线段树 下面就要根据数组来创建一棵线段树,我们的方法先创建下面的子线段树,然后由这些子线段树合并成大的线段树,以此类推
在合并左右子树的过程中,我们不能写死合并的过程,具体怎么合并应该由业务决定,由用户去决定如何合并,所以合并的过程我们写一个接口,具体的实现由用户去实现
public interface Merger <E > { public E merge (E a, E b) ; }
然后我们在构造方法中添加创建线段树的过程(为了创建线段树,增加了一个辅助方法)
private Merger<E> merger;public SegmentTree (E[] arr, Merger<E> merger) { this .merger = merger; data = (E[]) new Object[arr.length]; for (int i = 0 ; i < arr.length; i++) { data[i] = arr[i]; } tree = (E[]) new Object[4 * data.length]; buildSegmentTree(0 , 0 , data.length - 1 ); } private void buildSegmentTree (int treeIndex, int l, int r) { if (l == r) { tree[treeIndex] = data[l]; return ; } int leftTreeIndex = leftChild(treeIndex); int rightTreeIndex = rightChild(treeIndex); int mid = l + (r - l) / 2 ; buildSegmentTree(leftTreeIndex, l, mid); buildSegmentTree(rightTreeIndex, mid + 1 , r); tree[treeIndex] = merger.merge(tree[leftTreeIndex],tree[rightTreeIndex]); }
为了方便我们打印出线段树,我们实现一个 toString()
方法
@Override public String toString () { StringBuilder res = new StringBuilder(); res.append("[" ); for (int i = 0 ; i < tree.length; i++) { if (tree[i] != null ) { res.append(tree[i]); } else { res.append("null" ); } if (i != tree.length - 1 ) { res.append(", " ); } } res.append("]" ); return res.toString(); }
查询
实现代码
public E query (int queryL, int queryR) { if (queryL < 0 || queryL >= data.length || queryR < 0 || queryR >= data.length || queryL > queryR) { throw new IllegalArgumentException("参数错误" ); } return query(0 , 0 , data.length - 1 , queryL, queryR); } private E query (int treeIndex, int l, int r, int queryL, int queryR) { if (l == queryL && r == queryR) { return tree[treeIndex]; } int leftChildIndex = leftChild(treeIndex); int rightChildIndex = rightChild(treeIndex); int mid = l + (r - l) / 2 ; if (queryL >= mid + 1 ) { return query(rightChildIndex, mid+1 , r, queryL, queryR); } else if (queryR <= mid) { return query(leftChildIndex, l, mid, queryL, queryR); } E leftResult = query(leftChildIndex, l, mid, queryL, mid); E rightResult = query(rightChildIndex, mid + 1 , r, mid + 1 , queryR); return merger.merge(leftResult, rightResult); }
更新 public void set (int index, E e) { if (index < 0 || index >= data.length) { throw new IllegalArgumentException("参数错误" ); } set(0 , 0 , data.length - 1 , index, e); } private void set (int treeIndex, int l, int r, int index, E e) { if (l == r) { tree[treeIndex] = e; return ; } int leftChildIndex = leftChild(treeIndex); int rightChildIndex = rightChild(treeIndex); int mid = l + (r - l) / 2 ; if (index >= mid + 1 ) { set(rightChildIndex, mid+1 , r, index, e); } else { set(leftChildIndex, l, mid, index, e); } tree[treeIndex] = merger.merge(tree[leftChildIndex], tree[rightChildIndex]); }
完整代码 public class SegmentTree <E > { private E[] tree; private E[] data; private Merger<E> merger; public SegmentTree (E[] arr, Merger<E> merger) { this .merger = merger; data = (E[]) new Object[arr.length]; for (int i = 0 ; i < arr.length; i++) { data[i] = arr[i]; } tree = (E[]) new Object[4 * data.length]; buildSegmentTree(0 , 0 , data.length - 1 ); } private void buildSegmentTree (int treeIndex, int l, int r) { if (l == r) { tree[treeIndex] = data[l]; return ; } int leftTreeIndex = leftChild(treeIndex); int rightTreeIndex = rightChild(treeIndex); int mid = l + (r - l) / 2 ; buildSegmentTree(leftTreeIndex, l, mid); buildSegmentTree(rightTreeIndex, mid + 1 , r); tree[treeIndex] = merger.merge(tree[leftTreeIndex],tree[rightTreeIndex]); } public E query (int queryL, int queryR) { if (queryL < 0 || queryL >= data.length || queryR < 0 || queryR >= data.length || queryL > queryR) { throw new IllegalArgumentException("参数错误" ); } return query(0 , 0 , data.length - 1 , queryL, queryR); } private E query (int treeIndex, int l, int r, int queryL, int queryR) { if (l == queryL && r == queryR) { return tree[treeIndex]; } int leftChildIndex = leftChild(treeIndex); int rightChildIndex = rightChild(treeIndex); int mid = l + (r - l) / 2 ; if (queryL >= mid + 1 ) { return query(rightChildIndex, mid+1 , r, queryL, queryR); } else if (queryR <= mid) { return query(leftChildIndex, l, mid, queryL, queryR); } E leftResult = query(leftChildIndex, l, mid, queryL, mid); E rightResult = query(rightChildIndex, mid + 1 , r, mid + 1 , queryR); return merger.merge(leftResult, rightResult); } public void set (int index, E e) { if (index < 0 || index >= data.length) { throw new IllegalArgumentException("参数错误" ); } set(0 , 0 , data.length - 1 , index, e); } private void set (int treeIndex, int l, int r, int index, E e) { if (l == r) { tree[treeIndex] = e; return ; } int leftChildIndex = leftChild(treeIndex); int rightChildIndex = rightChild(treeIndex); int mid = l + (r - l) / 2 ; if (index >= mid + 1 ) { set(rightChildIndex, mid+1 , r, index, e); } else { set(leftChildIndex, l, mid, index, e); } tree[treeIndex] = merger.merge(tree[leftChildIndex], tree[rightChildIndex]); } public int getSize () { return data.length; } public E get (int index) { if (index < 0 || index >= data.length) { throw new IllegalArgumentException("参数错误" ); } return data[index]; } private int leftChild (int index) { return 2 * index + 1 ; } private int rightChild (int index) { return 2 * index + 2 ; } @Override public String toString () { StringBuilder res = new StringBuilder(); res.append("[" ); for (int i = 0 ; i < tree.length; i++) { if (tree[i] != null ) { res.append(tree[i]); } else { res.append("null" ); } if (i != tree.length - 1 ) { res.append(", " ); } } res.append("]" ); return res.toString(); } }
Trie Trie
树又称为字典树、前缀树。如果我们使用一般树结构去查询一个数据集里的单词,它的复杂度是 O(log n)
,但是如果我们使用 Trie
去查询单词的话,查询的复杂度只与单词的长度有关,与数据的规模无关。比如对于一个 $2^{20}$ 规模的数据集,我们去查一个单词 "word"
,一般树的复杂度为 O(20)
,而 Trie
树的复杂度为 O(4)
,其中 4 是单词的长度,所以 Trie
树是一种很高效的查询字符串的树结构。
class Node { char c; Node next[26 ]; }
但是这样考虑忽略了大小写,并且没有考虑一些特殊的字符,如 @
等符号或标点符号。所以我们每个节点不再是静态的指向 26 个节点,而是动态的指向若干个节点
class Node { char c; Map<Character,Node> next; }
另外我们通过某个字符来到一个节点,可以通过 Map
已经知道了,所以我们不必存储这个字符
class Node { Map<Character,Node> next; }
另外通过叶子节点是无法区别单词的结尾的,因为有的单词可能为某个单词的前缀,如 `"pan"` 为 `"panda"` 的前缀,所以我们要增加一个变量 `isWord` 来表示是否是单词的结尾
import java.util.TreeMap;public class Trie { private class Node { public boolean isWord; public TreeMap<Character,Node> next; public Node (boolean isWord) { this .isWord = isWord; next = new TreeMap<>(); } public Node () { this (false ); } } private Node root; private int size; public Trie () { root = new Node(); size = 0 ; } public int getSize () { return size; } }
实现 添加单词
public void add (String word) { Node cur = root; for (int i = 0 ; i < word.length(); i++) { char c = word.charAt(i); if (cur.next.get(c) == null ) { cur.next.put(c, new Node()); } cur = cur.next.get(c); } if (!cur.isWord) { cur.isWord = true ; size++; } }
查询单词 查询单词的逻辑与添加单词的逻辑高度重复,如果在查询过程中遇到没有指向该字符的节点,则直接返回 false
,如果遍历完毕都没有发生上面的情况,则判断该节点是否被标记为单词的结尾,如果没有则返回 false
,否则返回 true
。
public boolean contains (String word) { Node cur = root; for (int i = 0 ; i < word.length(); i++) { char c = word.charAt(i); if (cur.next.get(c) == null ) { return false ; } cur = cur.next.get(c); } return cur.isWord; }
前缀搜索 查询是否包含某个前缀,与 contains()
方法几乎一样,不过最后不用判断是否是单词结尾,直接返回 true
public boolean isPrefix (String prefix) { Node cur = root; for (int i = 0 ; i < prefix.length(); i++) { char c = prefix.charAt(i); if (cur.next.get(c) == null ) { return false ; } cur = cur.next.get(c); } return true ; }
简单字符匹配 对于字符串中的字符 .
规定它可以匹配任意的字符,那么这样的一个匹配算法如何写,如果我们遇到的字符不是 .
的话,逻辑和上面一样,如果遇到的是.
的话,我们就要去搜索该节点中所有的分叉(子树)
public boolean match (String word) { return match(root, word, 0 ); } private boolean match (Node node, String word, int index) { if (index == word.length()) { return node.isWord; } char c = word.charAt(index); if (c != '.' ) { if (node.next.get(c) == null ) { return false ; } else { return match(node.next.get(c), word, index + 1 ); } } else { for (char nextChar : node.next.keySet()) { if (match(node.next.get(nextChar), word, index + 1 )) { return true ; } } return false ; } }
全部代码 import java.util.TreeMap;public class Trie { private class Node { public boolean isWord; public TreeMap<Character,Node> next; public Node (boolean isWord) { this .isWord = isWord; next = new TreeMap<>(); } public Node () { this (false ); } } private Node root; private int size; public Trie () { root = new Node(); size = 0 ; } public int getSize () { return size; } public void add (String word) { Node cur = root; for (int i = 0 ; i < word.length(); i++) { char c = word.charAt(i); if (cur.next.get(c) == null ) { cur.next.put(c, new Node()); } cur = cur.next.get(c); } if (!cur.isWord) { cur.isWord = true ; size++; } } public boolean contains (String word) { Node cur = root; for (int i = 0 ; i < word.length(); i++) { char c = word.charAt(i); if (cur.next.get(c) == null ) { return false ; } cur = cur.next.get(c); } return cur.isWord; } public boolean isPrefix (String prefix) { Node cur = root; for (int i = 0 ; i < prefix.length(); i++) { char c = prefix.charAt(i); if (cur.next.get(c) == null ) { return false ; } cur = cur.next.get(c); } return true ; } public boolean match (String word) { return match(root, word, 0 ); } private boolean match (Node node, String word, int index) { if (index == word.length()) { return node.isWord; } char c = word.charAt(index); if (c != '.' ) { if (node.next.get(c) == null ) { return false ; } else { return match(node.next.get(c), word, index + 1 ); } } else { for (char nextChar : node.next.keySet()) { if (match(node.next.get(nextChar), word, index + 1 )) { return true ; } } return false ; } } }
并查集 我们之前遇到的树结构都是由父亲指向孩子,但是并查集不一样,它是由孩子指向父亲的一种结构,并查集结构可以非常高效的回答连接问题(Connectivity Problem),它可以很快的判断网络中节点的连接状态。并查集主要支持两个动作
union(p, q)
isConnected(p, q)
判断元素 p, q
是否是连接的,即是否所属一个集合
这里先给出并查集的接口,后面我们将实现多个版本的并查集
public interface UF { public int getSize () ; public boolean isConnected (int p, int q) ; public void unionElements (int p, int q) ; }
Quick Find
public class UnionFind1 implements UF { private int [] id; public UnionFind1 (int size) { id = new int [size]; for (int i = 0 ; i < id.length; i++) { id[i] = i; } } @Override public int getSize () { return id.length; } private int find (int p) { if (p < 0 || p >= id.length) { throw new IllegalArgumentException("参数错误" ); } return id[p]; } @Override public boolean isConnected (int p, int q) { return find(p) == find(q); } @Override public void unionElements (int p, int q) { if (find(p) == find(q)) { return ; } for (int i = 0 ; i < id.length; i++) { if (id[i] == find(p)) { id[i] = find(q); } } } }
Quick Union
public class UnionFind2 implements UF { private int [] parent; public UnionFind2 (int size) { parent = new int [size]; for (int i = 0 ; i < parent.length; i++) { parent[i] = i; } } @Override public int getSize () { return parent.length; } private int find (int index) { if (index < 0 || index >= parent.length) { throw new IllegalArgumentException("参数错误" ); } while (index != parent[index]) { index = parent[index]; } return index; } @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 ; } else { parent[pRoot] = parent[qRoot]; } } }
基于rank的优化
public class UnionFind3 implements UF { private int [] parent; private int [] rank; public UnionFind3 (int size) { parent = new int [size]; rank = new int [size]; for (int i = 0 ; i < parent.length; i++) { parent[i] = i; rank[i] = 1 ; } } @Override public int getSize () { return parent.length; } private int find (int index) { if (index < 0 || index >= parent.length) { throw new IllegalArgumentException("参数错误" ); } while (index != parent[index]) { index = parent[index]; } return index; } @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 ; } if (rank[pRoot] <= rank[qRoot]){ parent[pRoot] = parent[qRoot]; if (rank[pRoot] == rank[qRoot]) { rank[qRoot]++; } } else { parent[qRoot] = parent[pRoot]; } } }
路径压缩
public class UnionFind4 implements UF { private int [] parent; private int [] rank; public UnionFind4 (int size) { parent = new int [size]; rank = new int [size]; for (int i = 0 ; i < parent.length; i++) { parent[i] = i; rank[i] = 1 ; } } @Override public int getSize () { return parent.length; } private int find (int index) { if (index < 0 || index >= parent.length) { throw new IllegalArgumentException("参数错误" ); } while (index != parent[index]) { parent[index] = parent[parent[index]]; index = parent[index]; } return index; } @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 ; } if (rank[pRoot] <= rank[qRoot]){ parent[pRoot] = parent[qRoot]; if (rank[pRoot] == rank[qRoot]) { rank[qRoot]++; } } else { parent[qRoot] = parent[pRoot]; } } }
public class UnionFind5 implements UF { private int [] parent; private int [] rank; public UnionFind5 (int size) { parent = new int [size]; rank = new int [size]; for (int i = 0 ; i < parent.length; i++) { parent[i] = i; rank[i] = 1 ; } } @Override public int getSize () { return parent.length; } private int find (int index) { if (index < 0 || index >= parent.length) { throw new IllegalArgumentException("参数错误" ); } if (index != parent[index]) { parent[index] = find(parent[index]); } return parent[index]; } @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 ; } if (rank[pRoot] <= rank[qRoot]){ parent[pRoot] = parent[qRoot]; if (rank[pRoot] == rank[qRoot]) { rank[qRoot]++; } } else { parent[qRoot] = parent[pRoot]; } } }
第五版的效率不一定比第四版的好,因为第四版最后也可能做到”扁平化”,并且第五版的递归操作比较耗时。
AVL树 概念及实现 我们在研究二分搜索树时发现,如果我们将数据顺序添加进树中时,它有会退化成一棵链表,即所有的元素都添加到一个孩子上,这样树结构的优势就体现不出来,为了不使左右孩子的高度相差太大,我们需要对树进行调整,使树达到平衡,成为一棵平衡二叉树,AVL
就是一种经典的平衡二叉树
在 `AVL` 中,我们定义的平衡二叉树为,对于任意一个节点,左子树和右子树的高度相差不能超过 `1`。
我们为每一个节点标注好高度值,计算方法为取左右子树高度较高的高度,然后 +1
然后我们还有记录节点左右子树的高度差,我们称之为平衡因子(规定用左子树的高度-右子树的高度)
由于我们只是在添加元素和删除元素时对树进行调整,其余的代码同二分搜索树是相同的,所以就不贴出所有的代码,只给出不同的代码,首先我们需要在 `Node` 类中添加一个 `height` 变量来记录高度
private class Node { public E e; public Node left; public Node right; public int height; public Node (E e) { this .e = e; left = null ; right = null ; height = 1 ; } @Override public String toString () { return e.toString(); } }
新增加一个获得某节点高度的函数和平衡因子的函数
private int getHeight (Node node) { if (node == null ) { return 0 ; } return node.height; } private int getBalanceFactor (Node node) { if (node == null ) { return 0 ; } return getHeight(node.left) - getHeight(node.right); }
有了这些因素,我们一般需要在添加元素时进行维护,重新计算高度和平衡因子,从而进行调整
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 if (e.compareTo(node.e) > 0 ) { node.right = add(node.right,e); } node.height = Math.max(getHeight(node.left),getHeight(node.right)) + 1 ; int balanceFactor = getBalanceFactor(node); if (Math.abs(balanceFactor) > 1 ) { } return node; }
我们后面的内容主要是如何调整,后面所以只给出如何调整的代码,在学如何调整之前,我们来写两个辅助函数来判断这棵树是不是二分搜索树和 AVL
树,因为如果我们的代码有问题的话,有可能破坏二分搜索树的性质,这样有利于我们检查,那怎么检查一棵树是不是二分搜索树,我们根据二分搜索树的性质,它的中序遍历的结果是从小到大的特性,我们重写中序遍历为
public boolean isBST () { ArrayList<E> arrayList = new ArrayList<>(); inOrder(root, arrayList); for (int i = 1 ; i < arrayList.size(); i++) { if (arrayList.get(i-1 ).compareTo(arrayList.get(i)) > 0 ) return false ; } } return true ; } private void inOrder (Node node, ArrayList<E> arrayList) { if (node == null ) { return ; } inOrder(node.left, arrayList); arrayList.add(node.e); inOrder(node.right, arrayList); }
现在我们判断这棵树是不是平衡二叉树
public boolean isBalanced () { return isBalanced(root); } private boolean isBalanced (Node node) { if (node == null ) { return true ; } int balanceFactor = getBalanceFactor(node); if (Math.abs(balanceFactor) > 1 ) { return false ; } return isBalanced(node.left) && isBalanced(node.right); }
下面对不平衡的四种情形进行讨论,并给出调整方法
private Node rightRotate (Node y) { Node x = y.left; Node T3 = x.right; x.right = y; y.left = T3; y.height = Math.max(getHeight(y.left), getHeight(y.right)) + 1 ; x.height = Math.max(getHeight(x.left), getHeight(x.right)) + 1 ; return x; } private Node leftRotate (Node y) { Node x = y.right; Node T3 = x.left; x.left = y; y.right = T3; y.height = Math.max(getHeight(y.left), getHeight(y.right)) + 1 ; x.height = Math.max(getHeight(x.left), getHeight(x.right)) + 1 ; return x; } 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 if (e.compareTo(node.e) > 0 ) { node.right = add(node.right,e); } node.height = Math.max(getHeight(node.left),getHeight(node.right)) + 1 ; int balanceFactor = getBalanceFactor(node); if (balanceFactor > 1 && getBalanceFactor(node.left) >= 0 ) { return rightRotate(node); } if (balanceFactor < -1 && getBalanceFactor(node.right) <= 0 ) { return leftRotate(node); } if (balanceFactor > 1 && getBalanceFactor(node.left) < 0 ) { node.left = leftRotate(node.left); return rightRotate(node); } if (balanceFactor < -1 && getBalanceFactor(node.right) > 0 ) { node.right = rightRotate(node.right); return leftRotate(node); } return node; } public void remove (E e) { root = remove(root, e); } private Node remove (Node node, E e) { if (node == null ) { return null ; } Node retNode; if (e.equals(node.e)) { if (node.right == null ) { Node leftNode = node.left; node.left = null ; size--; retNode = leftNode; } else if (node.left == null ) { Node rightNode = node.right; node.right = null ; size--; retNode = rightNode; } else { Node successor = minimum(node.right); successor.right = remove(node.right,successor.e); successor.left = node.left; node.left = node.right = null ; retNode = successor; } } else if (e.compareTo(node.e) < 0 ) { node.left = remove(node.left, e); retNode = node; } else { node.right = remove(node.right, e); retNode = node; } if (retNode == null ) { return null ; } retNode.height = Math.max(getHeight(retNode.left),getHeight(retNode.right)) + 1 ; int balanceFactor = getBalanceFactor(retNode); if (balanceFactor > 1 && getBalanceFactor(retNode.left) >= 0 ) { return rightRotate(retNode); } if (balanceFactor < -1 && getBalanceFactor(retNode.right) <= 0 ) { return leftRotate(retNode); } if (balanceFactor > 1 && getBalanceFactor(retNode.left) < 0 ) { retNode.left = leftRotate(retNode.left); return rightRotate(retNode); } if (balanceFactor < -1 && getBalanceFactor(retNode.right) > 0 ) { retNode.right = rightRotate(retNode.right); return leftRotate(retNode); } return retNode; }
完整代码 import java.util.ArrayList;public class AVLTree <E extends Comparable <E >> { private class Node { public E e; public Node left; public Node right; public int height; public Node (E e) { this .e = e; left = null ; right = null ; height = 1 ; } @Override public String toString () { return e.toString(); } } private Node root; private int size; public AVLTree () { root = null ; size = 0 ; } public int size () { return size; } public boolean isEmpty () { return size == 0 ; } private int getHeight (Node node) { if (node == null ) { return 0 ; } return node.height; } private int getBalanceFactor (Node node) { if (node == null ) { return 0 ; } return getHeight(node.left) - getHeight(node.right); } public boolean isBST () { ArrayList<E> arrayList = new ArrayList<>(); inOrder(root, arrayList); for (int i = 1 ; i < arrayList.size(); i++) { if (arrayList.get(i-1 ).compareTo(arrayList.get(i)) > 0 ) { return false ; } } return true ; } private void inOrder (Node node, ArrayList<E> arrayList) { if (node == null ) { return ; } inOrder(node.left, arrayList); arrayList.add(node.e); inOrder(node.right, arrayList); } public boolean isBalanced () { return isBalanced(root); } private boolean isBalanced (Node node) { if (node == null ) { return true ; } int balanceFactor = getBalanceFactor(node); if (Math.abs(balanceFactor) > 1 ) { return false ; } return isBalanced(node.left) && isBalanced(node.right); } private Node rightRotate (Node y) { Node x = y.left; Node T3 = x.right; x.right = y; y.left = T3; y.height = Math.max(getHeight(y.left), getHeight(y.right)) + 1 ; x.height = Math.max(getHeight(x.left), getHeight(x.right)) + 1 ; return x; } private Node leftRotate (Node y) { Node x = y.right; Node T3 = x.left; x.left = y; y.right = T3; y.height = Math.max(getHeight(y.left), getHeight(y.right)) + 1 ; x.height = Math.max(getHeight(x.left), getHeight(x.right)) + 1 ; return x; } 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 if (e.compareTo(node.e) > 0 ) { node.right = add(node.right,e); } node.height = Math.max(getHeight(node.left),getHeight(node.right)) + 1 ; int balanceFactor = getBalanceFactor(node); if (balanceFactor > 1 && getBalanceFactor(node.left) >= 0 ) { return rightRotate(node); } if (balanceFactor < -1 && getBalanceFactor(node.right) <= 0 ) { return leftRotate(node); } if (balanceFactor > 1 && getBalanceFactor(node.left) < 0 ) { node.left = leftRotate(node.left); return rightRotate(node); } if (balanceFactor < -1 && getBalanceFactor(node.right) > 0 ) { node.right = rightRotate(node.right); return leftRotate(node); } return node; } public boolean contains (E e) { return contains(root, e); } private boolean contains (Node node, E e) { if (node == null ) { return false ; } 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); } } public E minimum () { if (size == 0 ) { throw new IllegalArgumentException("树为空" ); } return minimum(root).e; } private Node minimum (Node node) { if (node.left == null ) { return node; } return minimum(node.left); } public void remove (E e) { root = remove(root, e); } private Node remove (Node node, E e) { if (node == null ) { return null ; } Node retNode; if (e.equals(node.e)) { if (node.right == null ) { Node leftNode = node.left; node.left = null ; size--; retNode = leftNode; } else if (node.left == null ) { Node rightNode = node.right; node.right = null ; size--; retNode = rightNode; } else { Node successor = minimum(node.right); successor.right = remove(node.right,successor.e); successor.left = node.left; node.left = node.right = null ; retNode = successor; } } else if (e.compareTo(node.e) < 0 ) { node.left = remove(node.left, e); retNode = node; } else { node.right = remove(node.right, e); retNode = node; } if (retNode == null ) { return null ; } retNode.height = Math.max(getHeight(retNode.left),getHeight(retNode.right)) + 1 ; int balanceFactor = getBalanceFactor(retNode); if (balanceFactor > 1 && getBalanceFactor(retNode.left) >= 0 ) { return rightRotate(retNode); } if (balanceFactor < -1 && getBalanceFactor(retNode.right) <= 0 ) { return leftRotate(retNode); } if (balanceFactor > 1 && getBalanceFactor(retNode.left) < 0 ) { retNode.left = leftRotate(retNode.left); return rightRotate(retNode); } if (balanceFactor < -1 && getBalanceFactor(retNode.right) > 0 ) { retNode.right = rightRotate(retNode.right); return leftRotate(retNode); } return retNode; } }
红黑树 2-3树 2-3
树的节点它可以有一个元素,也可以有两个元素,它也满足二分搜索树的性质
我们把含有两个孩子的节点称为 `2` 节点,含有 `3` 个孩子的节点称为 `3` 节点
`2-3` 树是一种绝对平衡的树,所谓绝对平衡的树指的是从根节点到任意一个叶子节点,所经过的节点是都是相同的。那么 `2-3` 树是怎么做到的呢?
### 红黑树与2-3树的等价性
由于我们一般每个节点都是表示一个数据的,2-3
树有点难以实现,所以有人发明一种树叫做红黑树,它可以说是 2-3
树的等价,那么它树如何等价的呢?
上图想必很清楚的描述了等价的过程
现在我们来实现一下上面描述的红黑树,大部分的代码都是和二分搜索树是重合的,只是在添加时有调整,另外这里我们不牵涉到从红黑树中删除元素,因为太复杂了(其实是我不会)
public class RedBlackTree <E extends Comparable <E >> { private static final boolean RED = true ; private static final boolean BLACK = false ; private class Node { public E e; public Node left; public Node right; public boolean color; public Node (E e) { this .e = e; left = null ; right = null ; color = RED; } @Override public String toString () { return e.toString(); } } private Node root; private int size; public RedBlackTree () { root = null ; size = 0 ; } public int size () { return size; } public boolean isEmpty () { return size == 0 ; } public void add (E e) { root = add(root, e); } private boolean isRed (Node node) { if (node == null ) return BLACK; return node.color; } 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 if (e.compareTo(node.e) > 0 ) { node.right = add(node.right,e); } return node; } public boolean contains (E e) { return contains(root, e); } private boolean contains (Node node, E e) { if (node == null ) { return false ; } 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); } } }
红黑树的性质 在了解了红黑树与 2-3
等价以后,我们来看红黑树满足哪些性质
每个节点或者是红色的,或者的是黑色的
根节点是黑色的
- 每一个叶子节点(最后的空节点是黑色的)
- 因为红色节点只存在于 3 节点中,而所有的叶子节点都是 2 节点
- 如果一个节点是红色的,那么它的所有孩子节点都是黑色的
- 从任意一个节点到黑色节点,经过的黑色节点是一样的
- 因为 `2-3` 树到所有叶子节点的距离都是一样的,而经过的节点,不管是 `2` 节点还是 `3` 节点,都包括一个黑色节点,所以经过的黑色节点是一样的
向红黑树中添加元素 因为根节点是黑色的,所以我们在添加完元素后需要将根节点变为黑色
public void add (E e) { root = add(root, e); root.color = BLACK; }
在添加元素到红黑树中时,可能会破坏红黑树的规则,这时就需要红黑树进行自我调整,我们就来看一下添加过程会碰到的所有情形,以及处理方法
private Node leftRotate (Node node) { Node x = node.right; node.right = x.left; x.left = node; x.color = node.color; node.color = RED; return x; }
private void flipColors (Node node) { node.color = RED; node.left.color = BLACK; node.right.color = BLACK; }
private Node rightRotate (Node node) { Node x = node.left; node.left = x.right; x.right = node; x.color = node.color; node.color = RED; return x; }
对上面的情况进行总结
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 if (e.compareTo(node.e) > 0 ) { node.right = add(node.right,e); } if (isRed(node.right) && !isRed(node.left)) node = leftRotate(node); if (isRed(node.left) && isRed(node.left.left)) node = rightRotate(node); if (isRed(node.left) && isRed(node.right)) flipColors(node); return node; }
完整代码 public class RedBlackTree <E extends Comparable <E >> { private static final boolean RED = true ; private static final boolean BLACK = false ; private class Node { public E e; public Node left; public Node right; public boolean color; public Node (E e) { this .e = e; left = null ; right = null ; color = RED; } @Override public String toString () { return e.toString(); } } private Node root; private int size; public RedBlackTree () { root = null ; size = 0 ; } public int size () { return size; } public boolean isEmpty () { return size == 0 ; } private boolean isRed (Node node) { if (node == null ) { return BLACK; } return node.color; } private Node leftRotate (Node node) { Node x = node.right; node.right = x.left; x.left = node; x.color = node.color; node.color = RED; return x; } private Node rightRotate (Node node) { Node x = node.left; node.left = x.right; x.right = node; x.color = node.color; node.color = RED; return x; } private void flipColors (Node node) { node.color = RED; node.left.color = BLACK; node.right.color = BLACK; } public void add (E e) { root = add(root, e); root.color = BLACK; } 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 if (e.compareTo(node.e) > 0 ) { node.right = add(node.right,e); } if (isRed(node.right) && !isRed(node.left)) node = leftRotate(node); if (isRed(node.left) && isRed(node.left.left)) node = rightRotate(node); if (isRed(node.left) && isRed(node.right)) flipColors(node); return node; } public boolean contains (E e) { return contains(root, e); } private boolean contains (Node node, E e) { if (node == null ) { return false ; } 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); } } }
哈希表 我们通过将我们要查找的某种数据类型转化为一个索引 index
,然后通过索引去数组中查找,这时它的复杂度就是 O(1)
级别的。而将某个数据类型转化为索引的函数我们就称为是哈希函数,比如说将 26 个小写字母转化为索引,我们可以这么写
这样就建立起了一一对应的关系,但是并不是所有的对应关系都是一一对应的,因为数组的容量是有限的,而输入的范围可能是无穷的,所以很有可能不同的键对应着同一个索引,比如说键是字符串,因为字符串的组合方式是非常的多,可以看做是无穷的,我们不可能去开辟一个无穷的空间去与这些字符串一一对应,所以不同的字符串生成的索引很有可能会有冲突,我们称这种情况为哈希冲突。由于上面讲到的哈希冲突,所以我们要设计好哈希函数(hashCode()
)使得发生哈希冲突的可能性小,即使哈希函数产生的哈希值均匀的分布在数组中。
哈希函数的设计 哈希函数应该满足上面提到的:哈希函数产生的哈希值均匀的分布在数组中。数据的类型五花八门,对于特殊的领域有特殊领域的哈希函数的设计方式,甚至还有专门的论文,说这么多就是想说哈希函数的设计十分的复杂,在这里我们只提最简单的一种,哈希函数的设计应该满足
一致性
如果 a == b
,那么 hashCode(a) == hashCode(b)
高效性
均匀性
由于 `Java` 中基本数据类型和字符串类型有默认的 `hashCode()` 计算,所以我们就用 `Java` 自带的 `hashCode` 计算基本数据类型和字符串的哈希值,而对于引用类型 `Java` 是根据地址计算的哈希值,所以可能会出现问题,需要我们自己自定义规则,比如对于一个 `Student` 类,我们规定学号以及姓名相同(不区分大小写)就是同一个学生,所以根据一致性原则,它们应该产生相同的哈希值,但是由于 `Java` 默认是根据地址产生哈希值,由于二者的地址是不同的,所以产生的哈希值有极大的概率是不同的,所以我们需要自己创建哈希函数。
链地址法 现在我们来演示往哈希表中添加元素的步骤
import java.util.TreeMap;public class HashTable <K , V > { private TreeMap<K, V>[] hashTable; private int M; private int size; public HashTable (int M) { this .M = M; size = 0 ; hashTable = new TreeMap[M]; for (int i = 0 ; i < hashTable.length; i++) { hashTable[i] = new TreeMap<>(); } } public HashTable () { this (97 ); } public int getSize () { return size; } private int hash (K key) { return (key.hashCode() & 0x7fffffff ) % M; } public void add (K key, V value) { TreeMap<K, V> map = hashTable[hash(key)]; if (map.containsKey(key)) { map.put(key, value); } else { map.put(key, value); size++; } } public V remove (K key, V value) { V ret = null ; TreeMap<K, V> map = hashTable[hash(key)]; if (map.containsKey(key)) { ret = map.remove(key); size--; } return ret; } public void set (K key, V value) { TreeMap<K, V> map = hashTable[hash(key)]; if (!map.containsKey(key)) { throw new IllegalArgumentException("键不存在" ); } map.put(key,value); } public V get (K key) { return hashTable[hash(key)].get(key); } }
import java.util.TreeMap;public class HashTable <K , V > { private static final int upperTol = 10 ; private static final int lowerTol = 2 ; private static final int initCapacity = 7 ; private TreeMap<K, V>[] hashTable; private int M; private int size; public HashTable (int M) { hashTable = new TreeMap[initCapacity]; } public void add (K key, V value) { if (size >= upperTol * M) { resize(2 * M); } } public V remove (K key, V value) { if (size < M * lowerTol && M / 2 >= initCapacity) { resize(M / 2 ); } return ret; } private void resize (int newM) { TreeMap<K,V>[] newHashTable = new TreeMap[newM]; int oldM = M; M = newM; for (int i = 0 ; i < oldM; i++) { TreeMap<K, V> map = hashTable[i]; for (K key: map.keySet()) { newHashTable[hash(key)].put(key, map.get(key)); } } hashTable = newHashTable; } }
但是我们发现每次我们都扩容为 2 \* M
,这时 M
就不是一个素数了,为了解决这一个问题,我们准备一个素数表,让 M
取素数表中的值,每次扩容 M
在素数表中的索引 +1
,缩容 -1
import java.util.TreeMap;public class HashTable <K , V > { private static final int [] capacity = {}; private static final int upperTol = 10 ; private static final int lowerTol = 2 ; private int capacityIndex = 0 ; private TreeMap<K, V>[] hashTable; private int M; private int size; public HashTable () { this .M = capacity[capacityIndex]; size = 0 ; hashTable = new TreeMap[M]; for (int i = 0 ; i < hashTable.length; i++) { hashTable[i] = new TreeMap<>(); } } public void add (K key, V value) { if (size >= upperTol * M && capacityIndex + 1 < size) { capacityIndex++; resize(capacity[capacityIndex]); } } public V remove (K key, V value) { if (size < M * lowerTol && capacityIndex - 1 >= 0 ) { capacityIndex--; resize(capacity[capacityIndex]); } return ret; } }
完整代码 import java.util.TreeMap;public class HashTable <K , V > { private static final int [] capacity = {}; private static final int upperTol = 10 ; private static final int lowerTol = 2 ; private int capacityIndex = 0 ; private TreeMap<K, V>[] hashTable; private int M; private int size; public HashTable () { this .M = capacity[capacityIndex]; size = 0 ; hashTable = new TreeMap[M]; for (int i = 0 ; i < hashTable.length; i++) { hashTable[i] = new TreeMap<>(); } } public int getSize () { return size; } private int hash (K key) { return (key.hashCode() & 0x7fffffff ) % M; } public void add (K key, V value) { TreeMap<K, V> map = hashTable[hash(key)]; if (map.containsKey(key)) { map.put(key, value); } else { map.put(key, value); size++; } if (size >= upperTol * M && capacityIndex + 1 < capacity.length) { capacityIndex++; resize(capacity[capacityIndex]); } } public V remove (K key, V value) { V ret = null ; TreeMap<K, V> map = hashTable[hash(key)]; if (map.containsKey(key)) { ret = map.remove(key); size--; } if (size < M * lowerTol && capacityIndex - 1 >= 0 ) { capacityIndex--; resize(capacity[capacityIndex]); } return ret; } public void set (K key, V value) { TreeMap<K, V> map = hashTable[hash(key)]; if (!map.containsKey(key)) { throw new IllegalArgumentException("键不存在" ); } map.put(key,value); } public V get (K key) { return hashTable[hash(key)].get(key); } private void resize (int newM) { TreeMap<K,V>[] newHashTable = new TreeMap[newM]; int oldM = M; M = newM; for (int i = 0 ; i < oldM; i++) { TreeMap<K, V> map = hashTable[i]; for (K key: map.keySet()) { newHashTable[hash(key)].put(key, map.get(key)); } } hashTable = newHashTable; } }
参考链接