数据结构基础

一. 概述

1. 理解

1.1 数据结构与算法的关系

数据结构是一门研究组织数据方式的学科,有了编程语言也就有了数据结构。

程序 = 数据结构 + 算法

数据结构是算法的基础

1.2 线性结构和非线性结构

线性结构

  • 作为最常用的数据结构,特点是数据元素之间存在一对一的线性关系。

  • 包含两种不同的存储结构:顺序存储结构(如数组) 和 链式存储结构(如链表)。

  • 顺序存储的线性表称为顺序表,顺序表中的存储元素是连续的。

  • 链式存储的线性表称为链表,链表的存储元素不一定是连续的,元素节点中存放数据元素以及相邻元素的地址信息。

  • 线性结构常见的有:数组,链表,栈,队列,哈希表(散列表)。

非线性结构

  • 树形结构:二叉树,AVL树,红黑树,B树,堆,Trie,哈夫曼树,并查集...
  • 图形结构:邻接矩阵,邻接表...

2. 代码测试工具

2.1 测试某段代码的运行时间

public class TimeUtils { private static final SimpleDateFormat fmt = new SimpleDateFormat("HH:mm:ss.SSS"); public interface Task { void execute(); } public static void test(String title, Task task) { if (task == null) return; title = (title == null) ? "" : ("【" + title + "】"); System.out.println(title); System.out.println("开始:" + fmt.format(new Date())); long begin = System.currentTimeMillis(); task.execute(); long end = System.currentTimeMillis(); System.out.println("结束:" + fmt.format(new Date())); double delta = (end - begin) / 1000.0; System.out.println("耗时:" + delta + "秒"); System.out.println("-------------------------------------"); } }

2.2 断言工具

public class Asserts { public static void test(boolean value) { try { if (!value) throw new Exception("测试未通过"); } catch (Exception e) { e.printStackTrace(); } } }

2.3 Integer工具

public class IntegerUtils { /** 生成随机数 */ public static Integer[] random(int count, int min, int max) { if (count <= 0 || min > max) return null; Integer[] array = new Integer[count]; int delta = max - min + 1; for (int i = 0; i < count; i++) { array[i] = min + (int)(Math.random() * delta); } return array; } /** 合并两个数组 */ public static Integer[] combine(Integer[] array1, Integer[] array2) { if (array1 == null || array2 == null) return null; Integer[] array = new Integer[array1.length + array2.length]; for (int i = 0; i < array1.length; i++) { array[i] = array1[i]; } for (int i = 0; i < array2.length; i++) { array[i + array1.length] = array2[i]; } return array; } public static Integer[] same(int count, int unsameCount) { if (count <= 0 || unsameCount > count) return null; Integer[] array = new Integer[count]; for (int i = 0; i < unsameCount; i++) { array[i] = unsameCount - i; } for (int i = unsameCount; i < count; i++) { array[i] = unsameCount + 1; } return array; } /** * 生成头部和尾部是升序的数组 * disorderCount:希望多少个数据是无序的 */ public static Integer[] headTailAscOrder(int min, int max, int disorderCount) { Integer[] array = ascOrder(min, max); if (disorderCount > array.length) return array; int begin = (array.length - disorderCount) >> 1; reverse(array, begin, begin + disorderCount); return array; } /** * 生成中间是升序的数组 * disorderCount:希望多少个数据是无序的 */ public static Integer[] centerAscOrder(int min, int max, int disorderCount) { Integer[] array = ascOrder(min, max); if (disorderCount > array.length) return array; int left = disorderCount >> 1; reverse(array, 0, left); int right = disorderCount - left; reverse(array, array.length - right, array.length); return array; } /** * 生成头部是升序的数组 * disorderCount:希望多少个数据是无序的 */ public static Integer[] headAscOrder(int min, int max, int disorderCount) { Integer[] array = ascOrder(min, max); if (disorderCount > array.length) return array; reverse(array, array.length - disorderCount, array.length); return array; } /** * 生成尾部是升序的数组 * disorderCount:希望多少个数据是无序的 */ public static Integer[] tailAscOrder(int min, int max, int disorderCount) { Integer[] array = ascOrder(min, max); if (disorderCount > array.length) return array; reverse(array, 0, disorderCount); return array; } /** 升序生成数组 */ public static Integer[] ascOrder(int min, int max) { if (min > max) return null; Integer[] array = new Integer[max - min + 1]; for (int i = 0; i < array.length; i++) { array[i] = min++; } return array; } /** 降序生成数组 */ public static Integer[] descOrder(int min, int max) { if (min > max) return null; Integer[] array = new Integer[max - min + 1]; for (int i = 0; i < array.length; i++) { array[i] = max--; } return array; } /** 反转数组 */ private static void reverse(Integer[] array, int begin, int end) { int count = (end - begin) >> 1; int sum = begin + end - 1; for (int i = begin; i < begin + count; i++) { int j = sum - i; int tmp = array[i]; array[i] = array[j]; array[j] = tmp; } } /** 复制数组 */ public static Integer[] copy(Integer[] array) { return Arrays.copyOf(array, array.length); } /** 判断数组是否升序 */ public static boolean isAscOrder(Integer[] array) { if (array == null || array.length == 0) return false; for (int i = 1; i < array.length; i++) { if (array[i - 1] > array[i]) return false; } return true; } /** 打印数组 */ public static void println(Integer[] array) { if (array == null) return; StringBuilder string = new StringBuilder(); for (int i = 0; i < array.length; i++) { if (i != 0) string.append("_"); string.append(array[i]); } System.out.println(string); } }

二. 复杂度

1. 算法的效率问题

使用不同算法,解决同一个问题,效率可能相差非常大

1.1 求第n个斐波拉契数

  • 斐波那契数列的排列是:0,1,1,2,3,5,8,13,21,34,55,89,144...

  • 它后一个数等于前面两个数的和

image-20210801170249179
public class FibonacciNumber { public static void main(String[] args) { //耗时:4.674秒 TimeUtils.test("求第n个斐波那契数:fib1", new TimeUtils.Task() { @Override public void execute() { System.out.println(fib1(45)); } }); //耗时:0.0秒 TimeUtils.test("求第n个斐波那契数:fib2", new TimeUtils.Task() { @Override public void execute() { System.out.println(fib2(45)); } }); } /** * 实现一:递归 * 时间复杂度:O(2^n) */ public static int fib1(int n) { if (n <= 1) return n; return fib1(n - 1) + fib1(n - 2); } /** * 实现二:循环 * 时间复杂度:O(n) * <p> * 0,1,2,3,4,5,6 * 0,1,1,2,3,5,8,13 */ public static int fib2(int n) { if (n <= 1) return n; int first = 0, second = 1; // int sum = first + second; // first = second; // second = sum; second += first; first = second - first; } return second; } /** * 实现三:线性代数解法 – 特征方程 * 时间复杂度:可视为O(1) */ public static int fib3(int n) { double c = Math.sqrt(5); return (int) ((Math.pow((1 + c) / 2, n) - Math.pow((1 - c) / 2, n)) / c); } }

1.2 度量算法优劣的方法

事后统计

这种方法可行但是有两个问题:

  • 一是要想对设计的算法的运行性能进行评测,需要实际运行该程序。
  • 二是所得时间的统计量依赖于计算机的硬件、软件等环境因素, 这种方式,要在同一台计算机的相同状态下运行,才能比较那个算法速度更快。

事前估计

通过分析某个算法的时间复杂度,空间复杂度来判断哪个算法更优。

2 时间复杂度

2.1 理解

一般情况下,算法中的基本操作语句的重复执行次数是问题规模n的某个函数,用T(n)表示,若有某个辅助函数f(n),使得当n趋近于无穷大时,T(n) / f(n) 的极限值为不等于零的常数,则称f(n)是T(n)的同数量级函数。记作 T(n)=O( f(n) ),称O( f(n) ) 为算法的渐进时间复杂度,简称时间复杂度

T(n) 不同,但时间复杂度可能相同。 如:T(n)=n²+7n+6 与 T(n)=3n²+2n+2 它们的T(n) 不同,但时间复杂度相同,都为O(n²)。

2.2 大O表示法

一般用大O表示法来描述复杂度,它表示的是数据规模 n 对应的复杂度。如上述O( f(n) )

忽略常数、系数、低阶

  • 9 => O(1)

  • 2n + 3 => O(n)

  • n^2 + 2n + 6 => O(n^2 )

  • 4n^3 + 3n^2 + 22n + 100 => O(n^3 )

注意:大O表示法仅仅是一种粗略的分析模型,是一种估算,能帮助我们短时间内了解一个算法的执行效率

2.4 对数阶的细节

对数阶一般省略底数:log2(n) = log2(9) * log9(n)

所以 log2(n)、log9(n)统称为logn

2.5 计算时间复杂度的方法

  • 用常数1代替运行时间中的所有加法常数 T(n)=2n²+7n+6 => T(n)=2n²+7n+1

  • 修改后的运行次数函数中,只保留最高阶项 T(n)=2n²+7n+1 => T(n) = 2n²

  • 去除最高阶项的系数 T(n) = 2n² => T(n) = n² => O(n²)

2.6 常见的时间复杂度

  • 常数阶O(1)
  • 对数阶O(logn) //注意:底数不一定是2
  • 线性阶O(n)
  • 线性对数阶O(nlogn)
  • 平方阶O(n^2)
  • 立方阶O(n^3)
  • k次方阶O(n^k)
  • 指数阶O(2^n)
clipboard

说明

① 常见的算法时间复杂度由小到大依次为:Ο(1)<Ο(logn)<Ο(n)<Ο(nlogn) <Ο(n2)<Ο(n3)< Ο(n^k) <Ο(2^n) <Ο(n^n),随着问题规模n的不断增大,上述时间复杂度不断增大,算法的执行效率越低

② 从图中可见,我们应该尽可能避免使用指数阶的算法

③ 对数阶一般忽略底数,所以log2n,log9n统称logn

2.7 时间复杂度练习

public class TimeComplexityTest { public static void test1(int n) { // 1 if (n > 10) { System.out.println("n > 10"); } else if (n > 5) { // 2 System.out.println("n > 5"); } else { System.out.println("n <= 5"); } // 1 + 4 + 4 + 4 for (int i = 0; i < 4; i++) { System.out.println("test"); } // 14 => O(1) } public static void test2(int n) { // 1 + 3n => O(n) for (int i = 0; i < n; i++) { System.out.println("test"); } } public static void test3(int n) { // 1 + 2n + n * (1 + 45) // => 48n + 1 => O(n) for (int i = 0; i < n; i++) { for (int j = 0; j < 15; j++) { System.out.println("test"); } } } public static void test4(int n) { // n = 8 = 2^3 ,可以执行3次 // n = 16 = 2^4,可以执行4次 // => n = 2^k,可以执行log2(n)次 // log2(n) => O(logn) while ((n = n / 2) > 0) { System.out.println("test"); } } public static void test5(int n) { // log5(n) => O(logn) while ((n = n / 5) > 0) { System.out.println("test"); } } public static void test6(int n) { // i * 2^k = n // => k = log2(n/i) = log2(n) // 1 + log2(n) + log2(n) for (int i = 1; i < n; i = i * 2) { // 1 + 3n for (int j = 0; j < n; j++) { System.out.println("test"); } } // 1 + 2*log2(n) + log2(n) * (1 + 3n) // => 1 + 3*log2(n) + 2 * nlog2(n) // => O(nlogn) } public static void test7(int n) { // 1 + 2n + n * (1 + 3n) // => 3n^2 + 3n + 1 => O(n^2) for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { System.out.println("test"); } } } public static void test8(int n,int k) { //n for(int i = 0;i < n;i++) { System.out.println("test"); } //k for(int i = 0;i < k;i++) { System.out.println("test"); } //复杂度:O(n+k) } }

2.8 平均时间复杂度和最坏时间复杂度

平均时间复杂度是指所有可能的输入实例均以等概率出现的情况下,该算法的运行时间。

最坏情况下的时间复杂度称最坏时间复杂度。一般讨论的时间复杂度均是最坏情况下的时间复杂度。 这样做的原因是:最坏情况下的时间复杂度是算法在任何输入实例上运行时间的界限,这就保证了算法的运行时间不会比最坏情况更长

平均时间复杂度和最坏时间复杂度是否一致,和算法本身有关

2.9 均摊复杂度

什么情况下使用均摊复杂度:经过连续的多次复杂度比较低的情况后,出现个别复杂度比较高的情况。

案例:动态数组的扩容

2.10 复杂度震荡

什么是复杂度震荡:在一些特殊的情况下,某个级别的复杂度猛地蹿到了另一个级别,并且持续这一级别不恢复,则说明产生了复杂度震荡。

案例:动态数组扩容倍数、缩容时机设计不得当(扩容倍数*缩容倍数=1),有可能会导致复杂度震荡。

3. 空间复杂度

类似于时间复杂度的讨论,一个算法的空间复杂度(Space Complexity)定义为该算法所耗费的存储间,它也是问题规模n的函数。

空间复杂度(Space Complexity)是对一个算法在运行过程中临时占用存储空间大小的量度。有的算法需要占用的临时工作单元数与解决问题的规模n有关,它随着n的增大而增大,当n较大时,将占用较多的存储单元,例如快速排序和归并排序算法就属于这种情况.

在做算法分析时,主要讨论的是时间复杂度。从用户使用体验上看,更看重的程序执行的速度。一些缓存产品(redis, memcache)和算法(基数排序)本质就是用空间换时间。

4. 算法的优化方向

  • 用尽量少的存储空间

  • 用尽量少的执行步骤(执行时间)

  • 根据情况,可以选择空间换时间,也可以时间换空间

三. 线性结构

1. 动态数组ArrayList

1.1 理解

数组是一种顺序存储的线性表,所有元素的内存地址是连续的。

image-20210801210207171

在很多编程语言中,数组都有个致命的缺点:无法动态修改容量。实际开发中,我们更希望数组的容量是可以动态改变的

1.1 属性设计

image-20210801211343289

1.2 接口设计

注意与ArrayList源码对比分析

public interface List<E> { /** 元素数量 */ int size(); /** 是否为空 */ boolean isEmpty(); /** 是否包含某个元素 */ boolean contains(E element); /** 添加元素到末尾 */ void add(E element); /** 获取index位置的元素 */ E get(int index); /** 设置index位置的元素 */ E set(int index,E element); /** 往index位置添加元素 */ void add(int index,E element); /** 删除index位置对应的元素 */ E remove(int index); /** 查看元素的位置 */ int indexOf(E element); /** 清除所有元素 */ void clear(); }

1.3 图解方法

添加元素-add(E element)

image-20210801211557567

添加元素-add(int index,E element)

image-20210801211843586

删除元素-remove(int index)

image-20210801211723778

如何扩容

image-20210801212040012

1.4 实现

public class ArrayList<E> implements List<E>{ /** 元素数量 */ private int size = 0; /** 所有元素 */ private E[] elements; /** 默认容量 */ private static final int DEFAULT_CAPACITY = 10; /** 元素未找到返回的下标 */ private static final int ELEMENT_NOT_FOUND = -1; public ArrayList() { this(DEFAULT_CAPACITY); } public ArrayList(int capaticy) { capaticy = capaticy < DEFAULT_CAPACITY ? DEFAULT_CAPACITY : capaticy; elements = (E[])new Object[capaticy]; } /** * @Description 判断下标是否越界 */ private void indexCheck(int index) { if(index < 0 || index > size) { throw new IndexOutOfBoundsException("Index:" + index + ",Size:" + size); } } /** * @Description 数组容量不够则扩容 */ private void ensureCapacity(int size) { int oldCapacity = elements.length; if(size < oldCapacity) return; int newCapacity = oldCapacity + (oldCapacity >> 1);//1.5倍 E[] newElements = (E[])new Object[newCapacity]; // for (int i = 0; i < size; i++) { // newElements[i] = elements[i]; // } System.arraycopy(elements,0,newElements,0,elements.length); elements = newElements; System.out.println("扩容:" + oldCapacity + "=>" + newCapacity); } /** * @Description 数组容量太多则缩容 */ private void trim() { int oldCapacity = elements.length; if(size >= (oldCapacity >> 1)) return; if(oldCapacity <= DEFAULT_CAPACITY) return; //剩余空间很多,可以缩容 int newCapacity = oldCapacity >> 1; E[] newElements = (E[])new Object[newCapacity]; // for (int i = 0; i < size; i++) { // newElements[i] = elements[i]; // } System.arraycopy(elements,0,newElements,0,elements.length); elements = newElements; System.out.println("缩容:" + oldCapacity + "=>" + newCapacity); } /** * @Description 是否为空 * @return */ public boolean isEmpty() { return size == 0; } /** * @Description 元素的数量 * @return size */ public int size() { return size; } /** * @Description 往index位置添加元素 * @param index * @param element * @return */ public void add(int index, E element) { //最好复杂度:O(1)、最坏复杂度:O(n)、平均复杂度:O(n) indexCheck(index); ensureCapacity(size); for(int i = size;i > index;i--) { elements[i] = elements[i - 1]; } elements[index] = element; size++; } /** * @Description 添加元素到最后面 * @param element */ public void add(E element) { //最好:O(1) //最坏:O(n) => 扩容的情况 //平均:O(1) //均摊复杂度:O(1) =>把扩容情况均摊到每一种情况去 // (一般均摊等于最好)。 //什么情况下使用均摊复杂度:经过连续的多次复杂度比较低的 // 情况后,出现个别复杂度比较高的情况。 add(size, element); } /** * @Description 删除index位置对应的元素 * @param index * @return oldEle */ public E remove(int index) { //最好复杂度:O(1)、最坏复杂度:O(n)、平均复杂度:O(n) indexCheck(index); E oldEle = elements[index]; if(index != size - 1) { for(int i = index;i < size;i++) { elements[i] = elements[i + 1]; } } elements[--size] = null;//内存管理细节 trim(); //内存紧张考虑缩容 return oldEle; } /** * @Description 删除某个元素 * @param element */ public void remove(E element) { //O(1) remove(indexOf(element)); } /** * @Description 设置index位置的元素 * @param index * @param element * @return */ public E set(int index, E element) { //O(1) indexCheck(index); E old = elements[index]; elements[index] = element; return old; } /** * @Description 返回index位置对应的元素 * @param index * @return */ public E get(int index) { indexCheck(index); return elements[index]; } /** * @Description 查看元素的位置 * @param element * @return */ public int indexOf(E element) { if(element == null) { for (int i = 0; i < size; i++) { if(elements[i] == null) return i; } } else { for (int i = 0; i < size; i++) { if(element.equals(elements[i])) return i; } } return ELEMENT_NOT_FOUND; } /** * @Description 是否包含某个元素 * @param element * @return */ public boolean contains(E element) { return indexOf(element) != ELEMENT_NOT_FOUND; } /** * @Description 清除所有元素 */ public void clear() { //方法一:只是访问不到了,数组每一个位置对应的对象还存在。 // 当对某个位置再次add操作时,此位置存储的地址值对 // 应以前的对象才会被销毁。 //size = 0; //方法二:对每一个位置对应的对象地址值置空(内存管理细节) for (int i = 0; i < size; i++) { elements[i] = null; } size = 0; } @Override public String toString() { StringBuilder str = new StringBuilder(); str.append("size=").append(size).append(" : ["); for (int i = 0; i < size; i++) { if(i != 0) str.append(", "); str.append(elements[i]); } str.append("]"); return str.toString(); } }

2. 单向链表LinkedList

2.1 理解

动态数组有个明显的缺点:可能会造成内存空间的大量浪费。能否用到多少就申请多少内存?链表可以办到这一点。

链表存储结构的特点:

  • 链表是一种链式存储的线性表,通过指针域描述数据元素之间的逻辑关系,不需要地址连续的存储空间。
  • 动态存储空间分配,即时申请即时使用。
  • 访问第i个元素,必须顺序依此访问前面的1 ~ i-1的数据元素,也就是说是一种顺序存取结构。
    插入/删除操作不需要移动数据元素。

注意:

① Java中如何实现“指针”Java中的对象引用变量并不是存储实际数据,而是存储该对象在内存中的存储地址。

② 链表分为带头节点的链表和没有头节点的链表,根据实际的需求来确定。

2.2 图解方法

clipboard

2.3 实现

class SingleLinkedList<E> implements List<E>{ /** * 元素的数量 */ private int size; /** * 指向第一个节点的指针 */ private Node<E> first; /** * 元素未找到返回的下标 */ private static final int ELEMENT_NOT_FOUND = -1; /** * @Description 判断下标是否越界 */ private void indexCheck(int index) { if(index < 0 || index >= size) { throw new IndexOutOfBoundsException("Index:" + index + ",Size:" + size); } } /** * @Description 获取index位置对应的节点 * @return */ private Node<E> getNode(int index) { indexCheck(index); Node<E> temp = first; for(int i = 0;i < index;i++) { temp = temp.next; } return temp; } /** * @Description 是否为空 * @return */ public boolean isEmpty() { return size == 0; } /** * @Description 元素的数量 * @return size */ public int size() { return size; } /** * @Description 往index位置添加元素 * @param index * @param element * @return */ public void add(int index, E element) { if(index < 0 || index > size) { throw new IndexOutOfBoundsException("Index:" + index + ",Size:" + size); } if (index == 0) { first = new Node<>(element, first); } else { Node<E> prev = getNode(index - 1); prev.next = new Node<>(element, prev.next); } size++; } /** * @Description 添加元素到最后面 * @param element */ public void add(E element) { add(size, element); } /** * @Description 删除index位置对应的元素 * @param index * @return oldEle */ public E remove(int index) { indexCheck(index); Node<E> node = first; if (index == 0) { first = first.next; } else { Node<E> prev = getNode(index - 1); node = prev.next; prev.next = node.next; } size--; return node.element; } /** * @Description 设置index位置的元素 * @param index * @param element * @return */ public E set(int index, E element) { Node<E> node = getNode(index); E oldElement = node.element; node.element = element; return oldElement; } /** * @Description 返回index位置对应的元素 * @param index * @return */ public E get(int index) { return getNode(index).element; } /** * @Description 查看元素的位置 * @param element * @return */ public int indexOf(E element) { if (element == null) { Node<E> node = first; for (int i = 0; i < size; i++) { if (node.element == null) return i; node = node.next; } } else { Node<E> node = first; for (int i = 0; i < size; i++) { if (element.equals(node.element)) return i; node = node.next; } } return ELEMENT_NOT_FOUND; } /** * @Description 是否包含某个元素 * @param element * @return */ public boolean contains(E element) { return indexOf(element) != ELEMENT_NOT_FOUND; } /** * @Description 清除所有元素 */ public void clear() { size = 0; first = null; } @Override public String toString() { Node<E> temp = first; StringBuilder str = new StringBuilder(); for(int i = 0;i < size;i++) { if(i != 0) { str.append(","); } str.append(temp.element); temp = temp.next; } return "size=" + size + ", [" + str + "]"; } /** * @Description 节点内部类 */ private static class Node<E> { E element; Node<E> next; public Node(E element, Node<E> next) { this.element = element; this.next = next; } @Override public String toString() { return element + ""; } } }

3. 双向链表LinkedList

3.1 单向链表缺点

单向链表,查找的方向只能是一个方向,而双向链表可以向前或者向后查找。

单向链表不能自我删除,需要靠辅助节点 ,而双向链表,则可以自我删除,所以前面我们单链表删除节点,总是要先找到待删除节点的前一个节点。

3.2 实现

public class LinkedList<E> implements List<E>{ /** * 元素的数量 */ private int size; /** * 指向第一个节点的指针 */ private Node<E> first; /** * 指向最后一个节点的指针 */ private Node<E> last; /** * 元素未找到返回的下标 */ private static final int ELEMENT_NOT_FOUND = -1; /** * @Description 判断下标是否越界 */ private void indexCheck(int index) { if(index < 0 || index >= size) { throw new IndexOutOfBoundsException("Index:" + index + ",Size:" + size); } } /** * @Description 获取index位置对应的节点 * @return */ private Node<E> getNode(int index) { indexCheck(index); if(index < (size << 1)) { Node<E> temp = first; for(int i = 0;i < index;i++) { temp = temp.next; } return temp; } else { Node<E> temp = last; for(int i = size - 1;i > index;i--) { temp = temp.prev; } return temp; } } /** * @Description 是否为空 * @return */ public boolean isEmpty() { return size == 0; } /** * @Description 元素的数量 * @return size */ public int size() { return size; } /** * @Description 往index位置添加元素 * @param index * @param element * @return */ public void add(int index, E element) { if(index < 0 || index > size) { throw new IndexOutOfBoundsException("Index:" + index + ",Size:" + size); } if(index == size) { //往最后面添加元素时 Node<E> oldLast = last; last = new Node<E>(oldLast,element,null); if(oldLast == null) { //链表添加第一个元素时 first = last; } else { oldLast.next = last; } } else { Node<E> next = getNode(index); Node<E> prev = next.prev; Node<E> node = new Node<E>(prev,element,next); next.prev = node; if(prev == null) { //=>index == 0时 first = node; } else { prev.next = node; } } size++; } /** * @Description 添加元素到最后面 * @param element */ public void add(E element) { add(size, element); } /** * @Description 删除index位置对应的元素 * @param index * @return oldEle */ public E remove(int index) { indexCheck(index); Node<E> node = getNode(index); Node<E> prev = node.prev; Node<E> next = node.next; if(prev == null) { //index == 0 first = next; } else { prev.next = next; } if(next == null) { //index == size - 1 last = prev; } else { next.prev = prev; } size--; return node.element; } /** * @Description 设置index位置的元素 * @param index * @param element * @return */ public E set(int index, E element) { Node<E> node = getNode(index); E oldElement = node.element; node.element = element; return oldElement; } /** * @Description 返回index位置对应的元素 * @param index * @return */ public E get(int index) { return getNode(index).element; } /** * @Description 查看元素的位置 * @param element * @return */ public int indexOf(E element) { if (element == null) { Node<E> node = first; for (int i = 0; i < size; i++) { if (node.element == null) return i; node = node.next; } } else { Node<E> node = first; for (int i = 0; i < size; i++) { if (element.equals(node.element)) return i; node = node.next; } } return ELEMENT_NOT_FOUND; } /** * @Description 是否包含某个元素 * @param element * @return */ public boolean contains(E element) { return indexOf(element) != ELEMENT_NOT_FOUND; } /** * @Description 清除所有元素 */ public void clear() { size = 0; first = null; last = null; /* * gc root对象:① 被栈指针指向的对象,如new LinkedList() * * => 只要断掉first和last,当前链表不被gc root对象指向就 * 会被回收。 */ } @Override public String toString() { Node<E> temp = first; StringBuilder str = new StringBuilder(); for(int i = 0;i < size;i++) { if(i != 0) { str.append(","); } str.append(temp); temp = temp.next; } return "size=" + size + ", [" + str + "]"; } /** * @Description 节点内部类 */ private static class Node<E> { Node<E> prev; E element; Node<E> next; public Node(Node<E> prev,E element, Node<E> next) { this.prev = prev; this.element = element; this.next = next; } @Override public String toString() { StringBuilder str = new StringBuilder(); if(prev != null) { str.append(prev.element); } str.append("_").append(element).append("_"); if(next != null) { str.append(next.element); } return str + ""; } } }

3.3 ArrayList与LinkedList对比

ArrayList开辟,销毁内存空间的次数相对较少,但可能造成内存空间浪费(缩容解决)。LinkedList开辟、销毁内存空间的次数相对较多,但不会造成内存空间的浪费。

如果频繁在尾部进行添加,删除操作,动态数组与双向链表均可选择。

如果频繁在头部进行添加,删除操作,建议选择使用双向链表。

如果有频繁的(在任意位置)添加,删除操作,建议选择双向链表。

如果有频繁的查询操作(随机访问操作),建议选择动态数组。

是否有了双向链表,单向链表就没任何用处了? => 并非如此,在哈希表的设计中就用到了单链表。

4. 循环链表LinkedList

4.1 单向循环链表

注意:单向循环链表相对于单链表(SingleLinkedList)只需修改添加和删除。

/** * @Description 往index位置添加元素 * @param index * @param element * @return */ public void add(int index, E element) { if(index < 0 || index > size) { throw new IndexOutOfBoundsException("Index:" + index + ",Size:" + size); } if (index == 0) { Node<E> newFirst = new Node<>(element, first); //拿到最后一个节点 Node<E> last = (size == 0) ? newFirst : getNode(size - 1); last.next = newFirst; first = newFirst; } else { Node<E> prev = getNode(index - 1); prev.next = new Node<>(element, prev.next); } size++; } /** * @Description 删除index位置对应的元素 * @param index * @return oldEle */ public E remove(int index) { indexCheck(index); Node<E> node = first; if (index == 0) { if(size == 1) { first = null; } else { //拿到最后一个节点,注意一定要在改变first之前 Node<E> last = getNode(size - 1); first = first.next; last.next = first; } } else { Node<E> prev = getNode(index - 1); node = prev.next; prev.next = node.next; } size--; return node.element; }

4.2 双向循环链表

注意:双向循环链表相对于双向链表(LinkedList)只用修改添加和删除。

clipboard (1)
/** * @Description 往index位置添加元素 * @param index * @param element * @return */ public void add(int index, E element) { if(index < 0 || index > size) { throw new IndexOutOfBoundsException("Index:" + index + ",Size:" + size); } if(index == size) { //往最后面添加元素时 Node<E> oldLast = last; last = new Node<E>(oldLast,element,first); if(oldLast == null) { //链表添加第一个元素时 first = last; first.next = first; first.prev = first; } else { oldLast.next = last; first.prev = last; } } else { Node<E> next = getNode(index); Node<E> prev = next.prev; Node<E> node = new Node<E>(prev,element,next); next.prev = node; prev.next = node; if(index == 0) { //=>index == 0时 first = node; } } size++; } /** * @Description 删除index位置对应的元素 * @param index * @return node.element */ public E remove(int index) { indexCheck(index); Node<E> node = first; if(size == 1) { first = null; last = null; } else { node = getNode(index); Node<E> prev = node.prev; Node<E> next = node.next; prev.next = next; next.prev = prev; if(index == 0) { //index == 0 first = next; } if(index == size - 1) { //index == size - 1 last = prev; } } size--; return node.element; }

4.3 约瑟夫问题 (单向循环链表的应用)

约瑟夫问题:设编号为1,2,3...n的n个人围成一圈,约定编号为 k (1 <= k <= n)的人从1开始报数,数到m的那个人出列,它的下一位又从1开始报数,数到m的那个人又出列,依此类推,直到所有人出列为止,由此产生一个出列编号的序列。

注意:约瑟夫问题也可以用其它数据结构解决,不一定要用循环链表,但是循环链表解决此问题很简单。

clipboard (2)

使用循环链表解决约瑟夫问题

为了发挥循环链表的最大威力,可对CircleLinkedList做如下改进:

clipboard (3)
public class LinkedListTest { @Test public void test1() { CircleLinkedListForJosephus<Integer> list = new CircleLinkedListForJosephus<Integer>(); for(int i = 1; i <= 8;i++) { list.add(i); } //current指向头节点 list.reset(); while(!list.isEmpty()) { list.next(); list.next(); System.out.print(list.remove() + " ");//数了三次后删除 //3 6 1 5 2 8 4 7 } } } class CircleLinkedListForJosephus<E> implements List<E>{ /** * 元素的数量 */ private int size; /** * 指向第一个节点的指针 */ private Node<E> first; /** * 指向最后一个节点的指针 */ private Node<E> last; /** * 用于指向某个节点的指针 */ private Node<E> current; /** * 元素未找到返回的下标 */ private static final int ELEMENT_NOT_FOUND = -1; /** * @Description 判断下标是否越界 */ private void indexCheck(int index) { if(index < 0 || index >= size) { throw new IndexOutOfBoundsException("Index:" + index + ",Size:" + size); } } /** * @Description 获取index位置对应的节点 * @return */ private Node<E> getNode(int index) { indexCheck(index); if(index < (size << 1)) { Node<E> temp = first; for(int i = 0;i < index;i++) { temp = temp.next; } return temp; } else { Node<E> temp = last; for(int i = size - 1;i > index;i--) { temp = temp.prev; } return temp; } } /** * @Description 让current指向头节点 */ public void reset() { current = first; } /** * @Description 让current后移一步 * @return */ public E next() { if(current == null) return null; current = current.next; return current.element; } /** * @Description 删除current所指向的节点,并将current下移 * @return */ public E remove() { if(current == null) return null; Node<E> next = current.next; int index = indexOf(current.element); E element = remove(index); if(size == 0) { current = null; } else { current = next; } return element; } /** * @Description 是否为空 * @return */ public boolean isEmpty() { return size == 0; } /** * @Description 元素的数量 * @return size */ public int size() { return size; } /** * @Description 往index位置添加元素 * @param index * @param element * @return */ public void add(int index, E element) { if(index < 0 || index > size) { throw new IndexOutOfBoundsException("Index:" + index + ",Size:" + size); } if(index == size) { //往最后面添加元素时 Node<E> oldLast = last; last = new Node<E>(oldLast,element,first); if(oldLast == null) { //链表添加第一个元素时 first = last; first.next = first; first.prev = first; } else { oldLast.next = last; first.prev = last; } } else { Node<E> next = getNode(index); Node<E> prev = next.prev; Node<E> node = new Node<E>(prev,element,next); next.prev = node; prev.next = node; if(index == 0) { //=>index == 0时 first = node; } } size++; } /** * @Description 添加元素到最后面 * @param element */ public void add(E element) { add(size, element); } /** * @Description 删除index位置对应的元素 * @param index * @return node.element */ public E remove(int index) { indexCheck(index); Node<E> node = first; if(size == 1) { first = null; last = null; } else { node = getNode(index); Node<E> prev = node.prev; Node<E> next = node.next; prev.next = next; next.prev = prev; if(index == 0) { //index == 0 first = next; } if(index == size - 1) { //index == size - 1 last = prev; } } size--; return node.element; } /** * @Description 设置index位置的元素 * @param index * @param element * @return */ public E set(int index, E element) { Node<E> node = getNode(index); E oldElement = node.element; node.element = element; return oldElement; } /** * @Description 返回index位置对应的元素 * @param index * @return */ public E get(int index) { return getNode(index).element; } /** * @Description 查看元素的位置 * @param element * @return */ public int indexOf(E element) { if (element == null) { Node<E> node = first; for (int i = 0; i < size; i++) { if (node.element == null) return i; node = node.next; } } else { Node<E> node = first; for (int i = 0; i < size; i++) { if (element.equals(node.element)) return i; node = node.next; } } return ELEMENT_NOT_FOUND; } /** * @Description 是否包含某个元素 * @param element * @return */ public boolean contains(E element) { return indexOf(element) != ELEMENT_NOT_FOUND; } /** * @Description 清除所有元素 */ public void clear() { size = 0; first = null; last = null; /* * gc root对象:① 被栈指针指向的对象,如new LinkedList() * * => 只要断掉first和last,当前链表不被gc root对象指向就 * 会被回收。 */ } @Override public String toString() { Node<E> temp = first; StringBuilder str = new StringBuilder(); for(int i = 0;i < size;i++) { if(i != 0) { str.append(","); } str.append(temp); temp = temp.next; } return "size=" + size + ", [" + str + "]"; } /** * @Description 节点内部类 */ private static class Node<E> { Node<E> prev; E element; Node<E> next; public Node(Node<E> prev,E element, Node<E> next) { this.prev = prev; this.element = element; this.next = next; } @Override public String toString() { StringBuilder str = new StringBuilder(); if(prev != null) { str.append(prev.element); } str.append("_").append(element).append("_"); if(next != null) { str.append(next.element); } return str + ""; } } }

5. 栈(stack)

5.1 理解

栈是一个先入后出(FILO => First In Last Out)的有序列表。往栈中添加元素的操作,一般叫做入栈(push)。从栈中移除元素的操作,一般叫做 出栈(pop),注意只能移除栈顶元素,也叫做弹出栈顶元素。

栈是限制线性表中元素的插入和删除 只能在线性表的同一端 进行的一种特殊线性表。允许插入和删除的一端为变化的一端,称为 栈顶(Top),另一端为固定的一端,称为 栈底(Bottom)

出栈(pop)和入栈(push)的概念如下:

clipboard (4)

注意:这里说的“栈”与内存中的“栈空间”是两个不同的概念。

5.2 栈的应用场景

  • 子程序的调用:在跳往子程序前,会先将下一个指令的地址存到堆栈中,直到子程序执行完后再将地址取出,以回到原来的程序中。

  • 处理递归调用:和子程序的调用类似,只是除了存储下一个指令的地址外,也将参数,区域变量等数据存入堆栈中。

  • 表达式的转换与求值(实际解决):如中缀表达式转后缀表达式

  • 二叉树的遍历

  • 图形的深度优先(depth-first)搜索法

5.3 ArrayList模拟栈

public class StackTest { @Test public void test1() { ArrayListStack<Integer> stack = new ArrayListStack<Integer>(); stack.push(11); stack.push(22); stack.push(33); stack.push(44); System.out.println(stack.peek());//44 stack.list(); while(!stack.isEmpty()) { System.out.print(stack.pop() + " "); //44 33 22 11 } System.out.println(stack.isEmpty()); } } class ArrayListStack<E> { private List<E> list = new ArrayList<E>(); //栈的长度 public int size() { return list.size(); } // 判断栈空 public boolean isEmpty() { return list.isEmpty(); } // 入栈 public void push(E element) { list.add(element); } // 出栈 public E pop() { return list.remove(list.size() - 1); } // 获取栈顶元素 public E peek() { return list.get(list.size() - 1); } //遍历栈 public void list() { if(isEmpty()) { System.out.println("栈空,无数据!"); return; } for (int i = list.size() - 1; i >= 0; i--) { System.out.println("stack[" + i + "] = " + list.get(i)); } } }

5.4 LinkedList模拟栈

//只需要将以上代码的 private List<E> list = new ArrayList<E>(); //改为 private List<E> list = new LinkedList<E>();

5.5 栈的应用-综合计算器(自定义优先级)

即使用栈计算一个中缀表达式的结果

clipboard (5)
public class Calculator { public static void main(String[] args) { String expression = "7*2*2-5+1+4/2"; calculator(expression); //表达式 7*2*2-5+1+4/2 的结果为:26 } public static void calculator(String expression) { ArrayListStack2<Integer> numStack = new ArrayListStack2<>(); ArrayListStack2<Integer> operStack = new ArrayListStack2<>(); int index = 0;//用于扫描 int num1 = 0; int num2 = 0; int oper = 0; int res = 0; char ch = ' ';//将每次扫描得到的字符保存到ch String keepNum = "";//用于拼接多位数 //开始循环扫描expression while(true) { ch = expression.substring(index, index + 1).charAt(0); if(operStack.isOper(ch)) { if(!operStack.isEmpty()) { if(operStack.priority(ch) <= operStack.priority(operStack.peek())) { num1 = numStack.pop(); num2 = numStack.pop(); oper = operStack.pop(); res = numStack.cal(num1, num2, oper); //将运算结果入数栈 numStack.push(res); //将操作符入符号栈 operStack.push(ch + 0); } else { //当前操作符的优先级大于栈中的操作符优先级,直接入符号栈 operStack.push(ch + 0); } } else { //符号栈为空就直接入符号栈 operStack.push(ch + 0); } } else { //如果是数就直接入数栈 //numStack.push(ch - 48);//'1' => 1 (只能处理一位数) //能够处理多位数的思路: //当处理数时,需要向expression表达式的index后再看一位, //如果是数就拼接并继续扫描,是符号才入栈 keepNum += ch; if(index == expression.length() - 1) { //如果ch已经是expression的最后一位,就直接入栈 numStack.push(Integer.parseInt(keepNum)); } else { if(operStack.isOper(expression.substring(index + 1, index + 2) .charAt(0))) { numStack.push(Integer.parseInt(keepNum)); //将keepNum清空 keepNum = ""; } } } //使index + 1,并判断是否扫描到expression的最后 index++; if(index >= expression.length()) { break; } } //当表达式扫描完毕,就顺序的从数栈和符号栈中pop出相应的数和符号并运算 while(true) { if(operStack.isEmpty()) { break; } num1 = numStack.pop(); num2 = numStack.pop(); oper = operStack.pop(); res = numStack.cal(num1, num2, oper); numStack.push(res); } //将数栈的最后数pop出,得到结果 int res2 = numStack.pop(); System.out.println("表达式 " + expression + " 的结果为:" + res2); } } //数组模拟栈,需要扩展一些功能 class ArrayListStack2<E> { private List<E> list = new ArrayList<E>(); //栈的长度 public int size() { return list.size(); } // 判断栈空 public boolean isEmpty() { return list.isEmpty(); } // 入栈 public void push(E element) { list.add(element); } // 出栈 public E pop() { return list.remove(list.size() - 1); } // 获取栈顶元素 public E peek() { return list.get(list.size() - 1); } //遍历栈 public void list() { if(isEmpty()) { System.out.println("栈空,无数据!"); return; } for (int i = list.size() - 1; i >= 0; i--) { System.out.println("stack[" + i + "] = " + list.get(i)); } } //返回运算符的自定义优先级(假定优先级使用数字表示) public int priority(int oper) { if(oper == '*' || oper == '/') { return 1; } else if(oper == '+' || oper == '-') { return 0; } else { return -1;//假定目前的表达式只有+-*/ } } //判断当前字符是否是一个运算符 public boolean isOper(char val) { return val == '+' || val == '-' || val == '*' || val == '/'; } //计算两个操作数的方法 public int cal(int num1,int num2,int oper) { int res = 0; switch(oper) { case '+': res = num2 + num1; break; case '-': res = num2 - num1; break; case '*': res = num2 * num1; break; case '/': res = num2 / num1; break; default: break; } return res; } }

5.6 栈的应用-逆波兰计算器

逆波兰表达式(前缀表达式)

前缀表达式的运算符位于操作数之前。

前缀表达式的计算机求值:从右至左扫描表达式,遇到数字时,将数字压入堆栈,遇到运算符时,弹出栈顶的两个数,用运算符对它们做相应的计算(栈顶元素 和 次顶元素),并将结果入栈;重复上述过程直到表达式最左端,最后运算得出的值即为表达式的结果

//举例:(3+4)×5-6 对应的前缀表达式就是 - × + 3 4 5 6 , 针对前缀表达式求值步骤如下: //① 从右至左扫描,将6、5、4、3压入堆栈 //② 遇到+运算符,因此弹出3和4(3为栈顶元素,4为次顶元素),计算出3+4的值,得7,再将7入栈 //③ 接下来是×运算符,因此弹出7和5,计算出7×5=35,将35入栈 //④ 最后是-运算符,计算出35-6的值,即29,由此得出最终结果

中缀表达式

中缀表达式就是常见的运算表达式,如(3+4)×5-6

中缀表达式的求值是我们人最熟悉的,但是对计算机来说却不好操作(上述案例就能看的这个问题),因此,在计算结果时,往往会将中缀表达式转成其它表达式来操作(一般转成后缀表达式)

后缀表达式

与前缀表达式相似,只是运算符位于操作数之后

clipboard (6)

后缀表达式的计算机求值

从左至右扫描表达式,遇到数字时,将数字压入堆栈,遇到运算符时,弹出栈顶的两个数,用运算符对它们做相应的计算(次顶元素 和 栈顶元素),并将结果入栈;重复上述过程直到表达式最右端,最后运算得出的值即为表达式的结果

//例如: (3+4)×5-6 对应的后缀表达式就是 3 4 + 5 × 6 - , 针对后缀表达式求值步骤如下: //① 从左至右扫描,将3和4压入堆栈; //② 遇到+运算符,因此弹出4和3(4为栈顶元素,3为次顶元素),计算出3+4的值,得7,再将7入栈; //③ 将5入栈; //④ 接下来是×运算符,因此弹出5和7,计算出7×5=35,将35入栈; //⑤ 将6入栈; //⑥ 最后是-运算符,计算出35-6的值,即29,由此得出最终结果

中缀表达式转换为后缀表达式

后缀表达式适合计算式进行运算,但是人却不太容易写出来,尤其是表达式很长的情况下,因此在开发中,需要将中缀表达式转成后缀表达式

具体步骤:

//1.初始化两个栈:运算符栈s1和储存中间结果的栈s2; //2.从左至右扫描中缀表达式; //3.遇到操作数时,将其压s2; //4.遇到运算符时,比较其与s1栈顶运算符的优先级: // ① 如果s1为空,或栈顶运算符为左括号“(”,则直接将此运算符入栈; // ② 否则,若优先级比栈顶运算符的高,也将运算符压入s1; // ③ 否则,将s1栈顶的运算符弹出并压入到s2中,再次转到(4-1)与s1中新的栈顶 // 运算符相比较; //5.遇到括号时: // ① 如果是左括号“(”,则直接压入s1 // ② 如果是右括号“)”,则依次弹出s1栈顶的运算符,并压入s2,直到遇到左括号 // 为止,此时将这一对括号丢弃 //6.重复步骤2至5,直到表达式的最右边 //7.将s1中剩余的运算符依次弹出并压入s2 //8.依次弹出s2中的元素并输出,结果的逆序即为中缀表达式对应的后缀表达式
clipboard (7) clipboard (8)

使用栈实现逆波兰计算器(计算整数)

public class ReversePolishCalculate { //直接输入一个后缀表达式计算结果 @Test public void test1() { // 定义一个逆波兰表达式 // 为了方便,逆波兰表达式的数字和符号使用空格隔开 // (30+4)*5-6 => 30 4 + 5 * 6 - => 29 // 4*5-8+60+8/2 => 4 5 * 8 - 60 + 8 2 / + => 76 String suffixExpression = "4 5 * 8 - 60 + 8 2 / +"; // 思路: // ① 先将 "3 4 + 5 * 6 -" 放入ArrayList中 // ② 将ArrayList 传递给一个方法,遍历ArrayList配合栈完成计算 List<String> list = getListString(suffixExpression); System.out.println(list); int res = calculate(list); System.out.println("计算的结果是 " + res); } // 完成将一个中缀表达式转为后缀表达式的功能并计算结果 @Test public void test2() { // 思路: // ① 直接对str操作不方便,因此先将中缀表达式字符串转换为List // ② 中缀表达式对应的list => 后缀表达式对应的list String expression = "1+((2+3)*4)-5"; List<String> list = toInfixExpressionList(expression); System.out.println(list); // [1, +, (, (, 2, +, 3, ), *, 4, ), -, 5] List<String> list2 = parseSuffixExpressionList(list); System.out.println(list2); //[1, 2, 3, +, 4, *, +, 5, -] System.out.println("expression的计算结果为:" + calculate(list2));//16 } // 将一个中缀表达式转换成对应的List public static List<String> toInfixExpressionList(String s) { List<String> ls = new ArrayList<String>(); int i = 0;// 用于遍历中缀表达式字符串s的指针 String str;// 对多位数的拼接 char c;// 每遍历一个字符,就放入c do { // '0' => [48] '9' => [57] if ((c = s.charAt(i)) < 48 || (c = s.charAt(i)) > 57) { // 如果c是一个非数字,就加入ls中 ls.add("" + c); i++;// i需要后移 } else { // 如果是一个数字,需要考虑多位数 str = "";// 先将str置空 while (i < s.length() && (c = s.charAt(i)) > 48 && (c = s.charAt(i)) <= 57) { str += c;// 拼接 i++; } ls.add(str); } } while (i < s.length()); return ls; } //中缀表达式对应的list => 后缀表达式对应的list public static List<String> parseSuffixExpressionList(List<String> ls) { Stack<String> s1 = new Stack<String>();// 符号栈 // 注意:因为s2这个栈在整个转换过程中没有pop操作且后面要逆序输出, // 很麻烦,所以用ArrayList代替。 // Stack<String> s2 = new Stack<String>();//存储中间结果的栈 List<String> s2 = new ArrayList<String>();// 存储中间结果的List // 遍历ls for (String item : ls) { // 如果是一个数字,入s2 if (item.matches("\\d+")) { s2.add(item); } else if (item.equals("(")) { s1.push(item); } else if(item.equals(")")) { while(!s1.peek().equals("(")) { s2.add(s1.pop()); } s1.pop();//将"("弹出s1栈 } else { //思路:当item的优先级小于等于s1栈顶运算符时,将s1栈顶的运算符弹出 // 并加入s2中,再次与s1中新的栈顶运算符比较优先级。 //注意:我们需要一个比较运算符优先级高低的方法 while(s1.size() != 0 && Operation.getValue(s1.peek()) >= Operation.getValue(item)) { s2.add(s1.pop()); } //还需要将item压入栈中 s1.push(item); } } //将s1中剩余的运算符依次弹出并加入s2 while(s1.size() != 0) { s2.add(s1.pop()); } return s2;//注意因为是存放到List中的,所以按顺序输出即可 } // 将一个逆波兰表达式的数据和运算符放入ArrayList中 public static List<String> getListString(String suffixExpression) { // 将suffixExpression分隔 String[] split = suffixExpression.split(" "); ArrayList<String> list = new ArrayList<String>(); for (String ele : split) { list.add(ele); } return list; } // 完成对逆波兰表达式的运算 public static int calculate(List<String> ls) { Stack<String> stack = new Stack<String>(); for (String item : ls) { // 使用正则表达式取出数 if (item.matches("\\d+")) {// 匹配的是多位数 stack.push(item); } else { // pop出两个数运算,结果入栈 int num2 = Integer.parseInt(stack.pop()); int num1 = Integer.parseInt(stack.pop()); int res = 0; if (item.equals("+")) { res = num1 + num2; } else if (item.equals("-")) { res = num1 - num2; } else if (item.equals("*")) { res = num1 * num2; } else if (item.equals("/")) { res = num1 / num2; } else { throw new RuntimeException("运算符有误!"); } // 把res入栈 stack.push("" + res); } } // 最后留在stack中数据就是运算结果 return Integer.parseInt(stack.pop()); } } //返回一个运算符对应的优先级的类 class Operation { private static int ADD = 1; private static int SUB = 1; private static int MUL = 2; private static int DIV = 2; public static int getValue(String operation) { int result = 0; switch (operation) { case "+": result = ADD; break; case "-": result = SUB; break; case "*": result = MUL; break; case "/": result = DIV; break; default: System.out.println("不存在该运算符!"); break; } return result; } }

6.队列(queue)

6.1 理解

  • 队列是一个有序列表,可以用数组链表来实现。

  • 遵循先入先出的原则。即:先存入队列的数据,要先取出。后存入的要后取出

  • 队尾(rear):只能从队尾添加元素,一般叫做 入队(enQueue)

  • 队头(front):只能从队头移除元素,一般叫做 出队(deQueue)

clipboard (9)

注意:队列优先使用双向链表实现,因为队列主要是往头尾操作元素。

6.2 LinkedList(双向链表)模拟队列

Java官方使用LinkedList实现了Queue接口

public class LinkedListQueue<E> { private List<E> list = new LinkedList<E>(); //元素的数量 public int size() { return list.size(); } // 判断队列是否为空 public boolean isEmpty() { return list.isEmpty(); } // 入队 public void enQueue(E element) { list.add(element); } // 出队 public E deQueue() { return list.remove(0); } // 看一眼头部数据 public E peekFront() { return list.get(0); } //清空队列元素 public void clear() { list.clear(); } }

6.3 LinkedList模拟双端队列

deque => double ended queue

双端队列是能在头尾两端添加、删除的队列。

clipboard
public class LinkedListDeque<E> { private List<E> list = new LinkedList<E>(); // 元素的数量 public int size() { return list.size(); } // 判断队列是否为空 public boolean isEmpty() { return list.isEmpty(); } // 从队尾入队 public void enQueueRear(E element) { list.add(element); } // 从队头出队 public E deQueueFront() { return list.remove(0); } // 从队头入队 public void enQueueFront(E element) { list.add(0,element); } // 从队尾出队 public E deQueueRear() { return list.remove(list.size() - 1); } // 看一眼头部数据 public E peekront() { return list.get(0); } // 看一眼尾部数据 public E peekRear() { return list.get(list.size() - 1); } // 清空队列元素 public void clear() { list.clear(); } }

6.4 ArrayList模拟循环队列

其实队列底层也可以使用动态数组(ArrayList)实现,并且采用循环队列的方式各项接口也可以优化到 O(1) 的时间复杂度,这个用数组实现并且优化之后的队列也叫做:循环队列。

@SuppressWarnings("unchecked") public class ArrayListCircleQueue<E> { private int front;//存储队头下标 private E[] elements; private static final int DEFAULT_CAPACITY = 10; private int size; public ArrayListCircleQueue() { elements = (E[]) new Object[DEFAULT_CAPACITY]; } /** * @Description 数组容量不够则扩容 */ private void ensureCapacity(int capacity) { int oldCapacity = elements.length; if(capacity <= oldCapacity) return; int newCapacity = oldCapacity + (oldCapacity >> 1);//1.5倍 E[] newElements = (E[])new Object[newCapacity]; for (int i = 0; i < size; i++) { newElements[i] = elements[getRealIndex(i)]; } elements = newElements; //重置front front = 0; System.out.println("扩容:" + oldCapacity + "=>" + newCapacity); } /** * @Description 根据传入索引获取循环队列真实索引 * @param index * @return */ private int getRealIndex(int index) { //注意: // ① 尽量避免使用乘,除,模,浮点数运算,效率低下。 // ② 循环队列不会出现index为负数的情况,双端循环队列才会。 //return (front + index) % elements.length; index += front; //注意使用此方法要保证index不会大于等于element.length的两倍。 return index - (index >= elements.length ? elements.length : 0); } // 元素的数量 public int size() { return size; } // 判断队列是否为空 public boolean isEmpty() { return size == 0; } // 入队 public void enQueue(E element) { ensureCapacity(size + 1); elements[getRealIndex(size)] = element; size++; } // 出队 public E deQueue() { E frontElement = elements[front]; elements[front] = null; front = getRealIndex(1); size--; return frontElement; } // 显示队列的头数据 public E peek() { if(isEmpty()) { throw new RuntimeException("队列空,没有数据!"); } return elements[front]; } // 清空队列元素 public void clear() { for (int i = 0; i < size; i++) { elements[getRealIndex(i)] = null; } size = 0; front = 0; if(elements != null && elements.length > DEFAULT_CAPACITY) { elements = (E[]) new Object[DEFAULT_CAPACITY]; } } @Override public String toString() { StringBuilder str = new StringBuilder(); str.append("capcacity=").append(elements.length) .append(" front=").append(front) .append(" size=").append(size).append(",["); for(int i = 0;i < elements.length;i++) { if(i != 0) { str.append(","); } str.append(elements[i]); } str.append("]"); return str.toString(); } }

6.5 ArrayList模拟循环双端队列

@SuppressWarnings("unchecked") public class ArrayListCircleDeque<E> { private int front;//存储队头下标 private E[] elements; private static final int DEFAULT_CAPACITY = 10; private int size; public ArrayListCircleDeque() { elements = (E[]) new Object[DEFAULT_CAPACITY]; } /** * @Description 数组容量不够则扩容 */ private void ensureCapacity(int capacity) { int oldCapacity = elements.length; if(capacity <= oldCapacity) return; int newCapacity = oldCapacity + (oldCapacity >> 1);//1.5倍 E[] newElements = (E[])new Object[newCapacity]; for (int i = 0; i < size; i++) { newElements[i] = elements[getRealIndex(i)]; } elements = newElements; //重置front front = 0; System.out.println("扩容:" + oldCapacity + "=>" + newCapacity); } /** * @Description 根据传入索引获取循环队列真实索引 * @param index * @return */ private int getRealIndex(int index) { index += front; if(index < 0) { return index + elements.length; } return index - (index >= elements.length ? elements.length : 0); } // 元素的数量 public int size() { return size; } // 判断队列是否为空 public boolean isEmpty() { return size == 0; } // 从队尾入队 public void enQueueRear(E element) { ensureCapacity(size + 1); elements[getRealIndex(size)] = element; size++; } // 从队头出队 public E deQueueFront() { E frontElement = elements[front]; elements[front] = null; front = getRealIndex(1); size--; return frontElement; } // 从队头入队 public void enQueueFront(E element) { ensureCapacity(size + 1); front = getRealIndex(-1); elements[front] = element; size++; } // 从队尾出队 public E deQueueRear() { int realIndex = getRealIndex(size - 1); E rearElement = elements[realIndex]; elements[realIndex] = null; size--; return rearElement; } // 显示队列的头数据,注意不是取出数据 public E front() { return elements[front]; } // 显示队尾数据 public E rear() { return elements[getRealIndex(size - 1)]; } // 清空队列元素 public void clear() { for (int i = 0; i < size; i++) { elements[getRealIndex(i)] = null; } size = 0; front = 0; if(elements != null && elements.length > DEFAULT_CAPACITY) { elements = (E[]) new Object[DEFAULT_CAPACITY]; } } @Override public String toString() { StringBuilder str = new StringBuilder(); str.append("capcacity=").append(elements.length) .append(" front=").append(front) .append(" size=").append(size).append(",["); for(int i = 0;i < elements.length;i++) { if(i != 0) { str.append(","); } str.append(elements[i]); } str.append("]"); return str.toString(); } }

四. 递归

1. 理解

递归就是 方法自己调用自己,每次调用时传入不同的变量。递归有助于编程者解决复杂的问题,同时可以让代码变得简洁。

clipboard (1)

2. 应用场景

  • 各种数学问题如: 8皇后问题 , 汉诺塔, 阶乘问题, 迷宫问题, 球和篮子的问题(google编程大赛)
  • 各种算法中也会使用到递归,比如快排,归并排序,二分查找,分治算法等
  • 用栈解决的问题 => 使用递归可以让代码更简洁

3. 递归需要遵守的重要规则

  • 执行一个方法时,就创建一个 新的受保护的独立空间(栈空间)。
  • 方法的 局部变量是独立 的,不会相互影响, 比如上述n变量。
  • 如果方法中使用的是引用类型变量(比如数组),就会 共享该引用类型的数据
  • 递归 必须向退出递归的条件逼近,否则就是无限递归,出现StackOverflowError栈溢出了。
  • 当一个方法执行完毕,或者遇到return,就会返回,遵守 谁调用,就将结果返回给谁,同时当方法执行完毕或者返回时,该方法也就执行完毕。

4. 递归的应用-迷宫问题

clipboard (2)
public class MazeRetrospective { public static void main(String[] args) { //创建一个二维数组,模拟迷宫地图 int[][] map = new int[8][7]; //使用1代表墙 for(int i = 0;i < 7;i++) { map[0][i] = 1; map[7][i] = 1; } for(int i = 0;i < 8;i++) { map[i][0] = 1; map[i][6] = 1; } //设置挡板 map[2][1] = 1; map[2][2] = 1; map[2][3] = 1; map[5][4] = 1; map[5][5] = 1; //输出地图 System.out.println("初始化地图:"); for(int i = 0;i < 8;i++) { for(int j = 0;j < 7;j++) { System.out.print(map[i][j] + " "); } System.out.println(); } //使用递归回溯给小球找路 setWay(map, 1, 1); System.out.println("小球走过后的地图:"); for(int i = 0;i < 8;i++) { for(int j = 0;j < 7;j++) { System.out.print(map[i][j] + " "); } System.out.println(); } } /** * 使用递归回溯给小球找路 => * 说明:① 小球能到map[6][5]则说明通路找到。 * ② 当map[i][j]为0表示该点没有走过,当为1表示墙, * 3表示已经走过但是走不通。 * ③ 走迷宫时,需要确定一个策略(方法):下=>右=>上=>左, * 如果走不通再回溯 * @param map 表示地图 * @param i 表示从哪里走:(i,j) * @param j 表示从哪里走:(i,j) * @return 如果找到通路返回true,否则返回false */ public static boolean setWay(int[][] map,int i,int j) { if(map[6][5] == 2) { //通路已经找到 return true; } else { //如果当前点还没有走过 if(map[i][j] == 0) { //按照策略走 map[i][j] = 2;//假定该点可以走通 if(setWay(map,i + 1,j)) { //向下能走通 return true; } else if(setWay(map,i,j + 1)) { //向右能走通 return true; } else if(setWay(map,i - 1,j)) { //向上能走通 return true; } else if(setWay(map,i,j - 1)) { //向左能走通 return true; } else { //不能走通,该点是死路 map[i][j] = 3; return false; } } else { //如果map[i][j]不等0,则可能是1,2,3 return false; } } } } //结果: // 1 1 1 1 1 1 1 // 1 2 2 2 2 3 1 // 1 1 1 1 2 3 1 // 1 0 0 0 2 3 1 // 1 0 0 2 2 3 1 // 1 0 0 2 1 1 1 // 1 0 0 2 2 2 1 // 1 1 1 1 1 1 1

5. 递归的应用-八皇后问题(回溯算法)

clipboard (3)

思路分析

  • 第一个皇后先放第一行第一列。

  • 第二个皇后放在第二行第一列、然后判断是否OK, 如果不OK,继续放在第二列、第三列、依次把所有列都放完,找到一个合适的。

  • 继续第三个皇后,还是第一列、第二列……直到第8个皇后也能放在一个不冲突的位置,算是找到了一个正确解。

  • 当得到一个正确解时,在栈回退到上一个栈时就会开始回溯(一层一层的回溯)。即将第一个皇后放到第一列的所有正确解,全部得到。

  • 然后回头继续第一个皇后放第二列,后面继续循环执行上面的步骤。

说明:理论上应该创建一个二维数组来表示棋盘,但是实际上可以通过算法,用一个一维数组即可解决问题. arr[8] = {0 , 4, 7, 5, 2, 6, 1, 3} //对应arr 下标 表示第几行,即第几个皇后,arr[i] = val , val 表示第i+1个皇后,放在第i+1行的第val+1列。

实现

public class EightQueensHhess { int max = 8;//定义有多少个皇后 int[] array = new int[max];//数组array保存皇后放置位置的结果 static int count = 0; static int judgeCount = 0; public static void main(String[] args) { EightQueensHhess queen = new EightQueensHhess(); queen.check(0); System.out.println("一共有 " + count + " 种解法"); System.out.println("整个过程判断冲突的次数为 " + judgeCount + " 次"); } //放置第n个皇后的方法 //注意:check每一次递归时,进入check中都有for(int i = 0;i < max;i++), // 因此会进行回溯。 private void check(int n) { if(n == max) { //n从零开始,当n为8时就已经将8个皇后放置好了 show(); return; } //依次放入皇后并判断是否冲突 for(int i = 0;i < max;i++) { //先把当前这个皇后n放到该行第一列 array[n] = i; //判断当放置第n个皇后到i列时是否冲突 if(judge(n)) { //如果不冲突就放第n+1个皇后,即开始递归 check(n+1); } //注意:如果冲突就继续执行array[n] = i,即将第n个皇后放置 // 在本行后移的一个位置。 } } /** * @Description 查看当我们放置第n个皇后时,就去检测该皇后和前面已经 * 摆放的皇后是否存在冲突。 * @param n 表示第n个皇后 * @return */ private boolean judge(int n) { judgeCount++; for(int i = 0;i < n;i++) { //1.array[i] == array[n]表示判断第n个皇后是否和前面的n-1个 // 皇后在同一列。 //2.Math.abs(n-i) == Math.abs(array[n] - array[i]表示判断 // 第n个皇后是否和第i个皇后是否在同一斜线。 //3.没有必要判断是否在同一行,因为n每次都会递增。 if(array[i] == array[n] || Math.abs(n-i) == Math.abs(array[n] - array[i])) { return false; } } return true; } //将皇后摆放的位置输出的方法 private void show() { count++; for (int i = 0; i < array.length; i++) { System.out.print(array[i] + " "); } System.out.println(); } }

五. 树形结构

1. 概述

基本概念

  • 节点根节点父节点子节点兄弟节点(有相同的父节点)
  • 一棵树可以没有任何节点,称为 空树
  • 一棵树可以只有 1 个节点,也就是 只有根节点
  • 子树左子树右子树
  • 节点的度(degree):子树的个数
  • 树的度:所有节点度中的最大值
  • 叶子节点(leaf):度为 0 的节点
  • 非叶子节点:度不为 0 的节点
  • 层数(level):根节点在第 1 层,根节点的子节点在第 2 层,以此类推(有些说法也从第 0 层开始计算)
  • 节点的深度(depth):从根节点到当前节点的唯一路径上的节点总数
  • 节点的高度(height):从当前节点到最远叶子节点的路径上的节点总数
  • 树的深度:所有节点深度中的最大值
  • 树的高度:所有节点高度中的最大值
  • 树的深度 等于 树的高度
  • 树支路总数 = 树节点总数 - 1 (树中每个节点头上都有一个支路,但唯独根节点没有)

有序树,无序树,森林

  • 有序树:树中任意节点的子节点之间有顺序关系
  • 无序树:树中任意节点的子节点之间没有顺序关系,也称为“自由树”
  • 森林:由 m(m ≥ 0)棵互不相交的树组成的集合

2. 二叉树

2.1 特点

  • 每个节点的度最大为 2(最多拥有 2 棵子树)
  • 左子树和右子树是有顺序的
  • 即使某节点只有一棵子树,也要区分左右子树
  • 二叉树是度不大于2的有序树。但是度不大于2的有序树不是二叉树(因为有序树的节点次序是相对于另一节点而言的,当有序树的子树中只有一个孩子时,这个孩子节点无需区分左右次序,而二叉树无论孩子树是否为2,均需要确定左右次序)。

2.2 性质

  • 非空二叉树的第 i 层,最多有 2^( i − 1) 个节点( i ≥ 1 )
  • 在高度为 h 的二叉树上最多有 2^h − 1 个结点( h ≥ 1 )
// S=2^0 + 2^1 + 2^2 + 2^3 +..+ 2^(n-1) // 2S=2^1 + 2^2 + 2^3 +..+ 2^(n-1) + 2^n // 两式相减 // 2S-S=2^n - 2^0 // S=2^n - 1
  • 对于任何一棵非空二叉树,如果叶子节点个数为 n0,度为 2 的节点个数为 n2,则有: n0 = n2 + 1
//推导步骤: // ① 假设度为 1 的节点个数为 n1,那么二叉树的节点总数 n = n0 + n1 + n2 // ② 二叉树的支路数 T = n1 + 2 * n2 = n – 1 = n0 + n1 + n2 – 1 // ③ 因此 n0 = n2 + 1

2.3 真二叉树

理解:所有节点的度都要么为 0,要么为 2。

clipboard (4)

2.4 满二叉树

理解:最后一层节点的度都为 0,其他节点的度都为 2。

注意:

① 在同样高度的二叉树中,满二叉树的叶子节点数量最多、总节点数量最多。

② 满二叉树一定是真二叉树,真二叉树不一定是满二叉树。

clipboard (5)

2.5 完全二叉树

理解:对节点从上至下、左至右开始编号,其所有编号都能与相同高度的满二叉树中的编号对应。

注意:

① 叶子节点只会出现最后 2 层,最后 1 层的叶子结点都靠左对齐。

② 完全二叉树从根结点至倒数第 2 层是一棵满二叉树。

③ 满二叉树一定是完全二叉树,完全二叉树不一定是满二叉树。

clipboard (6)

性质:

// 1. 度为 1 的节点只有左子树。 // 2. 度为 1 的节点要么是 1 个,要么是 0 个 // 3. 同样节点数量的二叉树,完全二叉树的高度最小 // 4. 假设完全二叉树的高度为 h(h ≥ 1),那么: // ① 至少有 2^(h−1) 个节点 (2^0 + 2^1 + 2^2 + ⋯ + 2^(h−2) + 1 ) // ② 最多有 2^h − 1 个节点(2^0 + 2^1 + 2^2 + ⋯ + 2^(h−1),满二叉树) // ③ 当总节点数量为 n, // 可得:2^(h-1) <= n < 2^h // =》 h - 1 <= logn < h // =》 h = floor(logn) + 1 ※ //floor:向下取整 ceiling:向上取整 // 5. 一棵有 n 个节点的完全二叉树(n > 0),从上到下、从左到右对节点从 1 开始 // 进行编号,对任意第 i 个节点: // ① 如果 i = 1 ,它是根节点 // ② 如果 i > 1 ,它的父节点编号为 floor( i / 2 ) // ③ 如果 2i ≤ n ,它的左子节点编号为 2i // ④ 如果 2i > n ,它无左子节点 // ⑤ 如果 2i + 1 ≤ n ,它的右子节点编号为 2i + 1 // ⑥ 如果 2i + 1 > n ,它无右子节点 // 6. 一棵有 n 个节点的完全二叉树(n > 0),从上到下、从左到右对节点从 0 // 开始进行编号,对任意第 i 个节点: // ① 如果 i = 0 ,它是根节点 // ② 如果 i > 0 ,它的父节点编号为 floor( (i – 1) / 2 ) // ③ 如果 2i + 1 ≤ n – 1 ,它的左子节点编号为 2i + 1 // ④ 如果 2i + 1 > n – 1 ,它无左子节点 // ⑤ 如果 2i + 2 ≤ n – 1 ,它的右子节点编号为 2i + 2 // ⑥ 如果 2i + 2 > n – 1 ,它无右子节点

面试题:如果一棵完全二叉树有 768 个节点,求叶子节点的个数

// 解: 设叶子节点为 n0,度为2的节点为 n2,度为1的节点为 n1 // n = n0 + n1 + n2 // n0 = n2 + 1 // => n = 2n0 + n1 - 1 // 又因 完全二叉树度为1的节点要么是 1 个,要么是 0 个 // ① n1为1 时,n = 2n0,n必然为偶数。 // ② n1为0时,n = 2n0 - 1,n必然为奇数。 // => n0 = 768 / 2 = 384

由以上题总结公式:

  • 当总节点为偶数,n0 = n / 2。

  • 当总节点数为奇数,n0 = (n + 1) / 2

  • => n0 = floor( (n + 1) / 2 ) = ceiling( n / 2) 注意:java除法默认向下取整

判断一棵树是否为完全二叉树:

  • 思路一:
image-20211011003318480
  • 思路二:
image-20211011004513101

2.6 二叉树的遍历

遍历是数据结构中的常见操作:把所有元素都访问一遍

线性数据结构的遍历比较简单

  • 正序遍历
  • 逆序遍历

根据节点访问顺序的不同,二叉树的常见遍历方式有4种

  • 前序遍历(Preorder Traversal)
  • 中序遍历(Inorder Traversal)
  • 后序遍历(Postorder Traversal)
  • 层序遍历(Level Order Traversal)

前序遍历(递归/迭代)

访问顺序 :根节点、前序遍历左子树、前序遍历右子树

应用:树状结构展示(注意左右子树的顺序)

clipboard (7)

中序遍历(递归/迭代)

访问顺序 :中序遍历左子树、根节点、中序遍历右子树

应用:二叉搜索树的中序遍历按升序或降序处理节点

clipboard (8)

后序遍历(递归 / 迭代)

访问顺序 :后序遍历左子树、后序遍历右子树、根节点

应用:适用于一些先子后父的操作

clipboard (9)

层序遍历 (迭代实现:队列)

//实现思路 // 1. 将根节点入队 // 2. 循环执行以下操作,直到队列为空 // ① 将队头节点 A 出队,进行访问 // ② 将 A 的左子节点入队 // ③ 将 A 的右子节点入队

访问顺序 :从上到下、从左到右依次访问每一个节点

应用:① 计算二叉树的高度 ② 判断一棵树是否为完全二叉树

clipboard (10)

2.7 表达式树

四则运算的表达式可以分为3种

  • 前缀表达式(prefix expression),又称为波兰表达式
  • 中缀表达式(infix expression)
  • 后缀表达式(postfix expression),又称为逆波兰表达式
image-20220106233403750

如果将表达式的操作数作为叶子节点,运算符作为父节点(假设只是四则运算)

  • 这些节点刚好可以组成一棵二叉树
  • 比如表达式:A / B + C * D – E
image-20220106233524304

如果对这棵二叉树进行遍历

前序遍历

  • - + / A B * C D E
  • 刚好就是前缀表达式(波兰表达式)

中序遍历

  • A / B + C * D – E
  • 刚好就是中缀表达式(波兰表达式)

后序遍历

  • A B / C D * + E –
  • 刚好就是后缀表达式(逆波兰表达式)

2.8 二叉树遍历的应用

  • 前序遍历:树状结构展示(注意左右子树的顺序)
  • 中序遍历:二叉搜索树的中序遍历按升序或者降序处理节点
  • 后序遍历:适用于一些先子后父的操作
  • 层序遍历:①计算二叉树的高度。②判断一棵树是否为完全二叉树

2.9 根据遍历结果重构二叉树

  • 前序遍历 + 中序遍历 => 唯一的一颗二叉树
  • 后序遍历 + 中序遍历 => 唯一的一颗二叉树
  • 前序遍历 + 后序遍历 => 如果它是一棵真二叉树,结果是唯一的 。否则不然结果不唯一 。

2.10 前驱节点(predecessor)

理解:中序遍历时的前一个节点 “删除节点要使用该知识”

如果是二叉搜索树,前驱节点就是前一个比它小的节点

clipboard

2.11 后继节点(successor)

理解:中序遍历时的后一个节点 “删除节点要使用该知识”

如果是二叉搜索树,后继节点就是后一个比它大的节点

clipboard (1)

2.12 打印二叉树的工具

https://github.com/CoderMJLee/BinaryTrees

使用步骤:

  • 实现 BinaryTreeInfo 接口
  • 调用打印API :BinaryTrees.println(bst);

2.13 二叉树遍历非递归实现思路

前序遍历-非递归

利用栈实现(法一)

  • 设置 node = root
  • 循环执行以下操作
    • 如果 node != null
      • 对 node 进行访问
      • 将 node.right 入栈
      • 设置 node = node.left
    • 如果 node == null
      • 如果栈为空,结束遍历
      • 如果栈不为空,弹出栈顶元素并赋值给 node

利用栈实现(法二)

  • 将 root 入栈

  • 循环执行以下操作,直到栈为空

  • 弹出栈顶节点 top,进行访问

  • 将 top.right 入栈

  • 将 top.left 入栈

中序遍历-非递归

利用栈实现 :

  • 设置 node = root

  • 循环执行以下操作

    • 如果 node != null
      • 将 node 入栈
      • 设置 node = node.left
    • 如果 node == null
      • 如果栈为空,结束遍历
      • 如果栈不为空,弹出栈顶元素并赋值给 node
        • 对 node 进行访问
        • 设置 node = node.right

后序遍历 – 非递归

利用栈实现

  • 将 root 入栈
  • 循环执行以下操作,直到栈为空
    • 如果栈顶节点是叶子节点 或者 上一次访问的节点是栈顶节点的子节点 => 弹出栈顶节点,进行访问
    • 否则 => 将栈顶节点的right、left按顺序入栈

2.14 二叉树代码实现

/** * @Description 二叉树 * @Author monap * @Date 2021/10/10 20:10 */ @SuppressWarnings("unchecked") public class BinaryTree<E> implements BinaryTreeInfo { protected int size; /** * 根节点 */ protected Node<E> root; /** * 节点内部类 */ protected static class Node<E> { E element; Node<E> left; // 左子节点 Node<E> right; // 右子节点 Node<E> parent; // 父节点 /** * 左右节点可能没有,不必须 */ public Node(E element, Node<E> parent) { this.element = element; this.parent = parent; } public boolean isLeaf() { return left == null && right == null; } public boolean hasTwoChildren() { return left != null && right != null; } public boolean isLeftChild() { return parent != null && this == parent.left; } public boolean isRightChild() { return parent != null && this == parent.right; } public Node<E> getSibling() { if (isLeftChild()) { return parent.right; } if (isRightChild()) { return parent.left; } return null; } @Override public String toString() { String parentStr = "null"; if (parent != null) { parentStr = parent.element.toString(); } return element + "_P(" + parentStr + ")"; } } protected Node<E> createNode(E element, Node<E> parent) { return new Node<>(element, parent); } /** * 元素的数量 */ public int size() { return size; } /** * 是否为空 */ public boolean isEmpty() { return size == 0; } /** * 对外接口,用于传出去遍历到的元素(类似于Comparator定制排序) */ public static abstract class Visitor<E> { boolean stop; // 如果返回true就停止遍历 public abstract boolean visit(E element); } /** * 前序遍历(非递归方式)、使用栈实现 */ public void preorderTraversal(Visitor<E> visitor) { if (visitor == null || root == null) return; Node<E> node = root; Stack<Node<E>> stack = new Stack<>(); while (true) { if (node != null) { // 访问node节点 if (visitor.visit(node.element)) return; // 将右子节点入栈 if (node.right != null) { stack.push(node.right); } // 向左左 node = node.left; } else if (stack.isEmpty()) { return; } else { // 处理右边 node = stack.pop(); } } } /** * 前序遍历(递归方式) */ public void preorderTraversal2(Visitor<E> visitor) { if (visitor == null) return; preorderTraversal(root, visitor); } protected void preorderTraversal(Node<E> node, Visitor<E> visitor) { if (node == null || visitor.stop) { return; } // System.out.println(node.element); // visitor.visit(node.element);//定制 // if(visitor.stop) return; visitor.stop = visitor.visit(node.element); preorderTraversal(node.left, visitor); preorderTraversal(node.right, visitor); } /** * 中序遍历(非递归方式)、利用栈实现 */ public void inorderTraversal(Visitor<E> visitor) { if (visitor == null || root == null) return; Node<E> node = root; Stack<Node<E>> stack = new Stack<>(); while (true) { if (node != null) { stack.push(node); node = node.left; } else if (stack.isEmpty()) { return; } else { node = stack.pop(); // 访问node节点 if (visitor.visit(node.element)) return; // 让右节点进行中序遍历 node = node.right; } } } /** * 中序遍历(递归方式) */ public void inorderTraversal2(Visitor<E> visitor) { if (visitor == null) { return; } inorderTraversal(root, visitor); } protected void inorderTraversal(Node<E> node, Visitor<E> visitor) { if (node == null || visitor.stop) { return; } inorderTraversal(node.left, visitor); // visitor.visit(node.element); if (visitor.stop) { return; } visitor.stop = visitor.visit(node.element); inorderTraversal(node.right, visitor); } /** * 后序遍历(非递归方式)、利用栈实现 */ public void postorderTraversal(Visitor<E> visitor) { if (visitor == null || root == null) return; // 记录上一次弹出访问的节点 Node<E> prev = null; Stack<Node<E>> stack = new Stack<>(); stack.push(root); while (!stack.isEmpty()) { Node<E> top = stack.peek(); if (top.isLeaf() || (prev != null && prev.parent == top)) { prev = stack.pop(); // 访问node节点 if (visitor.visit(prev.element)) return; } else { if (top.right != null) { stack.push(top.right); } if (top.left != null) { stack.push(top.left); } } } } /** * 后序遍历(递归方式) */ public void postorderTraversal2(Visitor<E> visitor) { if (visitor == null) { return; } postorderTraversal(root, visitor); } protected void postorderTraversal(Node<E> node, Visitor<E> visitor) { if (node == null || visitor.stop) { // visitor.stop中止递归 return; } postorderTraversal(node.left, visitor); postorderTraversal(node.right, visitor); // visitor.visit(node.element); if (visitor.stop) { return; // 中止当前打印 ↓ } visitor.stop = visitor.visit(node.element); } /** * 层序遍历(迭代:队列) */ public void levelOrderTraversal(Visitor<E> visitor) { if (root == null || visitor == null) { return; } Queue<Node<E>> queue = new LinkedList<>(); // 将根节点入队 queue.offer(root); while (!queue.isEmpty()) { // 将头节点出队 Node<E> node = queue.poll(); // System.out.println(node.element); // visitor.visit(node.element); if (visitor.visit(node.element)) { return; } if (node.left != null) { queue.offer(node.left); } if (node.right != null) { queue.offer(node.right); } } } /** * 清空所有元素 */ public void clear() { root = null; size = 0; } //--------------------------- /** * 利用前序遍历进行简单的树状结构展示 */ @Override public String toString() { StringBuilder str = new StringBuilder(); toString(root, str, ""); return str.toString(); } protected void toString(Node<E> node, StringBuilder str, String prefix) { if (node == null) { return; } str.append(prefix).append(node.element).append("\n"); toString(node.left, str, prefix + "L-"); toString(node.right, str, prefix + "R-"); } //--------------------------- /** * 利用中序遍历求某个节点的前驱节点 */ protected Node<E> predecessor(Node<E> node) { if (node == null) { return null; } // 前驱节点在左子树中:node.left.right.right... Node<E> p = node.left; if (p != null) { while (p.right != null) { p = p.right; } return p; } // 从祖父节点中寻找前驱节点 while (node.parent != null && node == node.parent.left) { node = node.parent; } // 情况一:node.parent == null ↓ // 情况二:node == node.parent.right ↓ return node.parent; } /** * 利用中序遍历求某个节点的后继节点 */ protected Node<E> successor(Node<E> node) { if (node == null) { return null; } // 前驱节点在右子树中:node.right.left.left... Node<E> p = node.right; if (p != null) { while (p.left != null) { p = p.left; } return p; } // 从祖父节点中寻找前驱节点 while (node.parent != null && node == node.parent.right) { node = node.parent; } // 情况一:node.parent == null ↓ // 情况二:node == node.parent.left ↓ return node.parent; } //--------------------------- /** * 计算二叉树的高度(递归) */ public int heightByRecursion() { return height(root); } /** * 获取某一个节点的高度 */ protected int height(Node<E> node) { if (node == null) { return 0; } return 1 + Math.max(height(node.left), height(node.right)); } /** * 利用层序遍历计算二叉树的高度(迭代) */ public int heightByLevelOrderTraversal() { if (root == null) { return 0; } int height = 0; // 存储着每一层的元素数量 int levelSize = 0; Queue<Node<E>> queue = new LinkedList<>(); // 将根节点入队 queue.offer(root); while (!queue.isEmpty()) { levelSize = queue.size(); for (int i = 0; i < levelSize; i++) { Node<E> node = queue.poll(); assert node != null; if (node.left != null) { queue.offer(node.left); } if (node.right != null) { queue.offer(node.right); } } height++; } return height; } /** * 利用层序遍历判断一颗树是否为完全二叉树 */ public boolean isComplete() { if (root == null) { return false; } Queue<Node<E>> queue = new LinkedList<>(); // 将根节点入队 queue.offer(root); boolean leaf = false; while (!queue.isEmpty()) { // 将头节点出队 Node<E> node = queue.poll(); if (leaf && !node.isLeaf()) { return false; } if (node.left != null) { queue.offer(node.left); } else if (node.right != null) { //node.left == null && node.right != null return false; } if (node.right != null) { queue.offer(node.right); } else { //node.right == null leaf = true; } } return true; } /** * 使用前序遍历翻转二叉树(所有遍历方式都可实现) * * @param root * @return */ public Node<E> invertTree(Node<E> root) { if (root == null) { return null; } Node<E> tmp = root.left; root.left = root.right; root.right = tmp; invertTree(root.left); invertTree(root.right); return root; } //--------------------------- /** * 实现BinaryTreeInfo接口,进行高级的树状结构展示 */ @Override public Object root() { return root; } @Override public Object left(Object node) { return ((Node<E>) node).left; } @Override public Object right(Object node) { return ((Node<E>) node).right; } @Override public Object string(Object node) { return node; } }

3. 二叉搜索树

3.1 引入

在 n 个动态的整数中搜索某个整数?(查看其是否存在)。

  • 假设使用动态数组存放元素,从第 0 个位置开始遍历搜索,平均时间复杂度:O(n)。

    • 如果维护一个有序的动态数组,使用二分搜索,最坏时间复杂度:O(logn)。但是添加、删除的平均时间复杂度是 O(n)。

针对这个需求,有没有更好的方案?=> 使用二叉搜索树,添加、删除、搜索的最坏时间复杂度均可优化至:O(logn)级别 <==> O(h) 复杂度只与h有关

clipboard (2) clipboard (3)

3.2 理解

二叉搜索树是二叉树的一种,是应用非常广泛的一种二叉树,英文简称为 BST。

  • 又被称为:二叉查找树、二叉排序树

  • 任意一个节点的值都大于其左子树所有节点的值

  • 任意一个节点的值都小于其右子树所有节点的值

  • 它的左右子树也是一棵二叉搜索树

二叉搜索树可以大大提高搜索数据的效率

二叉搜索树存储的元素必须具备可比较性

  • 比如 int、double 等
  • 如果是自定义类型,需要指定比较方式
  • 不允许为 null
clipboard (4)

3.3 接口设计

由于二叉搜索树继承于二叉树,只需要实现添加,删除,包含的接口

clipboard (5)

注意:对于当前使用的二叉树来说,它的元素没有索引的概念。

3.4 图解

添加节点

clipboard (6)

删除节点

clipboard (7) clipboard (8) clipboard (9)

3.5 代码实现

/** * @Description 二叉搜索树 * @Author monap * @Date 2021/10/10 20:17 */ @SuppressWarnings("unchecked") public class BSTree<E> extends BinaryTree<E> { /** * 比较器定制排序 */ protected Comparator<E> comparator; public BSTree() { this(null); } public BSTree(Comparator<E> comparator) { this.comparator = comparator; } /** * 检查添加元素是否为空 */ protected void elementNoNullCheck(E element) { if (element == null) { throw new IllegalArgumentException("element must no be null!"); } } /** * 比较元素大小,返回值为0代表e1等于e2,大于0代表e1大于e2,小于0代表e1小于e2 */ protected int compare(E e1, E e2) { if (comparator != null) { return comparator.compare(e1, e2); } // 注意:不在上面写死(BinarySearchTree<E extends Comparable>), // 而是在这里进行强制转换,如果E没有实现此接口就会报错提醒。否则 // 如果E没有实现此接口在编译时就会报错,我们希望两种比较方式都可以使用。 return ((Comparable<E>) e1).compareTo(e2); } /** * 添加元素 */ public void add(E element) { elementNoNullCheck(element); // 添加第一个节点(根节点) if (root == null) { root = createNode(element, null); size++; // 新添加节点之后的处理 afterAdd(root); return; } // 如果添加的不是第一个节点: // 1.找到待添加位置的父节点 Node<E> parent = root; Node<E> node = root; int cmp = 0; while (node != null) { cmp = compare(element, node.element); parent = node; if (cmp > 0) { node = node.right; } else if (cmp < 0) { node = node.left; } else { // 一般覆盖(不同对象可能有相同的比较参数) node.element = element; return; } } // 2.判断插入父节点的左子节点还是右子节点 Node<E> newNode = createNode(element, parent); if (cmp > 0) { parent.right = newNode; } else { parent.left = newNode; } size++; // 新添加节点之后的处理 afterAdd(newNode); } /** * 添加node节点后所需要做的调整(二叉搜索树不需要调整) */ protected void afterAdd(Node<E> node) { } /** * 删除元素 */ public void remove(E element) { remove(node(element)); } /** * 根据元素找到对应节点 */ protected Node<E> node(E element) { Node<E> node = root; while (node != null) { int cmp = compare(element, node.element); if (cmp == 0) { return node; } if (cmp > 0) { node = node.right; } else { node = node.left; } } return null; } /** * 删除对应节点 */ protected void remove(Node<E> node) { if (node == null) { return; } size--; // 考虑度为2的节点,转化为度为1 if (node.hasTwoChildren()) { // 后继节点 Node<E> s = successor(node); // 用后继节点的值覆盖度为2的节点的值 node.element = s.element; // 删除后继节点 node = s; } // 删除node节点(能到这则说明node的度必为0或1) Node<E> replacement = node.left != null ? node.left : node.right; // node是度为1的节点 if (replacement != null) { //更改parent replacement.parent = node.parent; // 更改parent的left,right指向 // node是度为1的节点也是根节点 if (node.parent == null) { root = replacement; } else if (node == node.parent.left) { node.parent.left = replacement; } else { // 在右边 node.parent.right = replacement; } // 此时开始恢复平衡(AVL树 或 RB树需要实现此方法) afterRemove(node, replacement); } else if (node.parent == null) { // node是叶子节点也是根节点 root = null; afterRemove(node, null); } else { // node是叶子节点但不是根节点 if (node == node.parent.left) { node.parent.left = null; } else { node.parent.right = null; } // 此时开始恢复平衡(AVL树 或RB树 需要实现此方法) afterRemove(node, null); } } /** * 删除node节点后所需要做的调整(二叉搜索树不需要调整) */ protected void afterRemove(Node<E> node, Node<E> replacement) { } /** * 是否包含某元素 */ public boolean contains(E element) { return node(element) != null; } }

3.6 测试

public class BSTreeTest { @Test public void test() { //测试二叉树打印工具 // ┌─_A_─┐ // │ │ // _B_ _C_ BinaryTrees.println(new BinaryTreeInfo() { @Override public Object string(Object node) { return "_" + node.toString() + "_"; } @Override public Object root() { return "A"; } @Override public Object right(Object node) { if(node.equals("A")) return "C"; return null; } @Override public Object left(Object node) { if(node.equals("A")) return "B"; return null; } }); } @Test public void test1() { // 自然排序 Integer[] data = new Integer[] { 7, 4, 9, 2, 5, 8, 11, 3, 12, 1 }; BSTree<Integer> bst = new BSTree<>(); for (int i = 0; i < data.length; i++) { bst.add(data[i]); } BinaryTrees.println(bst, PrintStyle.INORDER); String str = BinaryTrees.printString(bst); Files.writeToFile("D:/1.txt", str); } @Test public void test2() { // 定制排序 Integer[] data = new Integer[] { 7, 4, 9, 2, 5, 8, 11, 3, 12, 1 }; BSTree<Person> bst = new BSTree<>(new Comparator<Person>() { @Override public int compare(Person e1, Person e2) { return e2.getAge() - e1.getAge(); } }); for (int i = 0; i < data.length; i++) { bst.add(new Person(data[i])); } BinaryTrees.println(bst); } @Test public void test3() { //遍历测试 Integer[] data = new Integer[] { 7, 4, 2, 1, 3, 5, 9, 8, 11, 10, 12}; BSTree<Integer> bst = new BSTree<>(); for (int i = 0; i < data.length; i++) { bst.add(data[i]); } BinaryTrees.println(bst); BinaryTrees.println(bst); //测试前序遍历(递归) System.out.println("前序遍历:"); bst.preorderTraversal(new Visitor<Integer>() { public boolean visit(Integer element) { System.out.print(element + " "); return element == 2 ? true : false;//遍历到值为2停止 } }); System.out.println(); //测试中序遍历(递归) System.out.println("中序遍历:"); bst.inorderTraversal(new Visitor<Integer>() { public boolean visit(Integer element) { System.out.print(element + " "); return element == 4 ? true : false;//遍历到值为4停止 } }); System.out.println(); //测试后序遍历(递归) System.out.println("后序遍历:"); bst.postorderTraversal(new Visitor<Integer>() { public boolean visit(Integer element) { System.out.print(element + " "); return element == 4 ? true : false;//遍历到值为4停止 } }); System.out.println(); //测试层序序遍历(队列) System.out.println("层序遍历:"); bst.levelOrderTraversal(new Visitor<Integer>() { public boolean visit(Integer element) { System.out.print(element + "_ "); return false;//遍历所有 } }); } @Test public void test4() { //遍历的应用测试 Integer[] data = new Integer[] { 7, 4, 9, 5, 2}; BSTree<Integer> bst = new BSTree<>(); for (int i = 0; i < data.length; i++) { bst.add(data[i]); } BinaryTrees.println(bst); //1.前序遍历的应用:展示树状结构(其他遍历方式也可以展示树状结构) System.out.println(bst); //2.层序遍历的应用:计算二叉树的高度 System.out.println("二叉树高度为:" + bst.height1());//递归方式 System.out.println("二叉树高度为:" + bst.height2());//层序递归方式 //3.层序遍历的应用:判断一颗树是否是完全二叉树 System.out.println("当前二叉搜索树是否为完全二叉树:" + bst.isComplete()); Integer[] data1 = new Integer[] { 7, 4, 9, 2, 1}; BSTree<Integer> bst1 = new BSTree<>(); for (int i = 0; i < data1.length; i++) { bst1.add(data1[i]); } BinaryTrees.println(bst1); System.out.println("当前二叉搜索树是否为完全二叉树:" + bst1.isComplete()); } @Test public void test5() { //测试删除 Integer[] data = new Integer[] { 7, 4, 9, 2, 5, 8, 11, 3, 12, 1 }; BSTree<Integer> bst = new BSTree<>(); for (int i = 0; i < data.length; i++) { bst.add(data[i]); } BinaryTrees.println(bst); bst.remove(7); BinaryTrees.println(bst); } } class Person implements Comparable<Person> { private int age; public Person() { } public Person(int age) { this.age = age; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } @Override public int compareTo(Person e) { return age - e.age; } @Override public String toString() { return "age=" + age; } }
public class BSTreeTest { @Test public void comparatorTest() { BSTree<Person> personBSTree = getPersonBSTree(); BinaryTrees.println(personBSTree, BinaryTrees.PrintStyle.INORDER); BinaryTrees.println(personBSTree, BinaryTrees.PrintStyle.LEVEL_ORDER); } @Test public void traversalTest() { BSTree<Person> personBSTree = getPersonBSTree(); BinaryTrees.println(personBSTree, BinaryTrees.PrintStyle.LEVEL_ORDER); personBSTree.preorderTraversal(new BinaryTree.Visitor<Person>() { @Override public boolean visit(Person element) { // element.setHeight(element.getHeight() + 1); System.out.println(element.getHeight()); if (element.getHeight() == 2.05) { return true; } return false; } }); } @Test public void preorderTraversalPrintTest() { BSTree<Person> personBSTree = getPersonBSTree(); System.out.println(personBSTree.toString()); } private BSTree<Person> getPersonBSTree() { BSTree<Person> personBSTree = new BSTree<>(new Comparator<Person>() { @Override public int compare(Person o1, Person o2) { return o1.getAge().compareTo(o2.getAge()); } }); personBSTree.add(new Person("张三",20,1.85)); personBSTree.add(new Person("李四",16,2.05)); personBSTree.add(new Person("王二",77,1.54)); personBSTree.add(new Person("莫言",64,1.99)); return personBSTree; } private class Person { private String name; private Integer age; private Double height; public Person() { } public Person(String name, Integer age, Double height) { this.name = name; this.age = age; this.height = height; } public String getName() { return name; } public void setName(String name) { this.name = name; } public Integer getAge() { return age; } public void setAge(Integer age) { this.age = age; } public Double getHeight() { return height; } public void setHeight(Double height) { this.height = height; } @Override public String toString() { return age.toString(); } } }

4. AVL树

4.1 理解

平衡因子(Balance Factor):某结点的左右子树的高度差(左 - 右)

AVL树的特点:

  • 每个节点的平衡因子只可能是 1、0、-1(绝对值 ≤ 1,如果超过 1,称之为“失衡”)

  • 每个节点的左右子树高度差不超过 1

  • 搜索、添加、删除的时间复杂度是 O(logn)

说明:红黑树的添加删除后的旋转恢复平衡都是O(1)级别的。AVL树添加后的旋转恢复平衡是O(1)级别的,而删除后的旋转恢复平衡操作的最坏情况达到了O(logn)级别

clipboard

注意:

① AVL树是最早发明的自平衡二叉搜索树之一

② AVL 取名于两位发明者的名字 :G. M. Adelson-Velsky 和 E. M. Landis(来自苏联的科学家)

4.2 继承机构

clipboard (1)

4.3 添加导致的失衡

示例:往下面这棵子树中添加 13

  • 最坏情况:可能会导致所有祖先节点都失衡

  • 父节点、非祖先节点,都不可能失衡

clipboard (2)

四种添加失衡情况及其处理(有且仅有四种)

  • LL-g右旋转(单旋)
clipboard (3)
  • RR-g左旋转(单旋)
clipboard (4)
  • LR-p左旋转,g右旋转(双旋)
clipboard (5)
  • RL-p右旋转,g左旋转(双旋)
clipboard (6)

四种添加失衡情况的统一处理

clipboard (7)

4.4 删除导致的失衡

示例:删除下面这棵树的16

clipboard (8)

删除后需要使用 LL-右旋转 解决失衡的情况

  • 如果绿色节点不存在,更高层的祖先节点可能也会失衡,需要再次恢复平衡,然后又可能导致更高层的祖先节点失衡...
  • 极端情况下,所有祖先节点都需要进行恢复平衡的操作,共 O(logn) 次调整
clipboard

删除后需要使用 RR-左旋转 解决失衡的情况

clipboard (1)

删除后需要使用 LR-p左旋转,g右旋转(双旋) 解决失衡的情况

clipboard (2)

删除后需要使用 RL-p右旋转,g左旋转(双旋) 解决失衡的情况

clipboard (3)

4.5 总结

添加

  • 可能会导致 所有祖先节点 都失衡

  • 只要让高度最低的失衡节点恢复平衡,整棵树就恢复平衡【仅需 O(1) 次调整

删除

  • 可能会导致父节点或祖先节点失衡(只有1个节点会失衡

  • 恢复平衡后,可能会导致更高层的祖先节点失衡【最多需要 O(logn) 次调整

平均时间复杂度

  • 搜索:O(logn)

  • 添加:O(logn),仅需 O(1) 次的旋转操作

  • 删除:O(logn),最多需要 O(logn) 次的旋转操作

4.6 代码实现

平衡二叉搜索树

/** * @Description 平衡二叉搜索树 * @author Polaris * @version * @date 2020年3月10日下午8:33:51 */ public class BBSTree<E> extends BSTree<E>{ public BBSTree() { this(null); } public BBSTree(Comparator<E> comparator) { super(comparator); } /** * 左旋转,以RR为例 */ protected void rotateLeft(Node<E> grand) { Node<E> parent = grand.right; Node<E> child = parent.left;//child就是T1子树 grand.right = child; parent.left = grand; afterRotate(grand, parent, child); } /** * 右旋转,以LL为例 */ protected void rotateRight(Node<E> grand) { Node<E> parent = grand.left; Node<E> child = parent.right; grand.left = child; parent.right = grand; afterRotate(grand, parent, child); } /** * 抽取左旋转和右旋转中的重复代码 */ protected void afterRotate(Node<E> grand,Node<E> parent,Node<E> child) { //更新parent的parent(让parent成为子树的根节点) parent.parent = grand.parent; if(grand.isLeftChild()) { grand.parent.left = parent; } else if(grand.isRightChild()) { grand.parent.right = parent; } else { //grand是root节点 root = parent; } //更新child的parent if(child != null) { child.parent = grand; } //更新grand的parent grand.parent = parent; } /** * 统一旋转 */ protected void rotate( Node<E> r, //之前的根节点 Node<E> a,Node<E> b,Node<E> c, Node<E> d, Node<E> e,Node<E> f,Node<E> g) { //让d成为这棵子树的根节点 d.parent = r.parent; if(r.isLeftChild()) { r.parent.left = d; } else if(r.isRightChild()) { r.parent.right = d; } else { root = d; } //处理a,b,c之间的关系 b.left = a; if(a != null) { a.parent = b; } b.right = c; if(c != null) { c.parent = b; } //处理e,f,g之间的关系 f.left = e; if(e != null) { e.parent = f; } f.right = g; if(g != null) { g.parent = f; } //处理b,d,f之间的关系 d.left = b; d.right = f; b.parent = d; f.parent = d; } }

AVL树

/** * @Description AVL树 * @author Polaris * @version * @date 2020年3月10日下午8:35:05 */ public class AVLTree<E> extends BBSTree<E> { public AVLTree() { this(null); } public AVLTree(Comparator<E> comparator) { super(comparator); } /** * AVL树特有的节点,多了height属性用于计算平衡因子 */ private static class AVLNode<E> extends Node<E> { //每个新添加的未经过处理的节点必然是叶子节点(高度默认为 1) int height = 1;//AVL树平衡因子:左子树高度 - 右子树高度(默认为叶子节点的高度1) public AVLNode(E element, Node<E> parent) { super(element, parent); } /* * 更新当前节点自己的高度 */ public void updateHeight() { int leftHeight = left == null ? 0 : ((AVLNode<E>)left).height; int rightHeight = right == null ? 0 : ((AVLNode<E>)right).height; height = 1 + Math.max(leftHeight, rightHeight); } /* * 获取当前节点的平衡因子 */ public int balanceFactor() { int leftHeight = left == null ? 0 : ((AVLNode<E>)left).height; int rightHeight = right == null ? 0 : ((AVLNode<E>)right).height; return leftHeight - rightHeight; } /* * 获取当前节点高度更高一点的子树 */ public Node<E> tallerChild() { int leftHeight = left == null ? 0 : ((AVLNode<E>)left).height; int rightHeight = right == null ? 0 : ((AVLNode<E>)right).height; if(leftHeight > rightHeight) return left; if(leftHeight < rightHeight) return right; //当左右子树相等时,就返回和当前节点同方向(比如当前节点parent的左子树)的子树 return isLeftChild() ? left :right; } @Override public String toString() { String parentString = "null"; if (parent != null) { parentString = parent.element.toString(); } return element + "_p(" + parentString + ")_h(" + height + ")"; } } /** * 重写createNode,用于创建AVL特有的AVL节点 */ @Override protected Node<E> createNode(E element, Node<E> parent) { return new AVLNode<>(element, parent); } /** * 实现添加新节点后的处理操作(通过当前节点找到失衡节点进行调整) */ @Override protected void afterAdd(Node<E> node) { while ((node = node.parent) != null) { if (isBalanced(node)) { //如果平衡 //更新高度(如果采用递归更新高度效率太差,直接在找parent失衡节点时就更新高度) updateHeight(node); } else { //如果不平衡(记得要在恢复平衡时更新高度) // 恢复平衡 rebalance(node); break;//找到一个不平衡节点恢复平衡则整棵树都平衡 } } } @Override protected void afterRemove(Node<E> node,Node<E> replacement) { while ((node = node.parent) != null) { if (isBalanced(node)) { //如果平衡 //更新高度(如果采用递归更新高度效率太差,直接在找parent失衡节点时就更新高度) updateHeight(node); } else { //如果不平衡(记得要在恢复平衡时更新高度) // 恢复平衡 rebalance(node); } } } /** * 判断当前节点是否平衡 */ private boolean isBalanced(Node<E> node) { return Math.abs(((AVLNode<E>)node).balanceFactor()) <= 1; } /** * 更新某个节点的高度(将强制转换封装为方法) */ private void updateHeight(Node<E> node) { ((AVLNode<E>)node).updateHeight(); } //————————方式一:分开处理—————————— /** * 恢复平衡(四种失衡情况单独处理) * @param node 高度最低的那个不平衡节点 */ private void rebalance(Node<E> grand) { //p是g左右子树中高度较高的子树 Node<E> parent = ((AVLNode<E>)grand).tallerChild(); //n是p左右子树中高度较高的子树 Node<E> node = ((AVLNode<E>)parent).tallerChild(); if(parent.isLeftChild()) { //L if(node.isLeftChild()) { //LL rotateRight(grand);//g左旋转 } else { //LR rotateLeft(parent);//p左旋转 rotateRight(grand);//g右旋转 } } else { //R if(node.isLeftChild()) { //RL rotateRight(parent);//p右旋转 rotateLeft(grand);//g左旋转 } else { //RR rotateLeft(grand); } } } @Override protected void afterRotate(Node<E> grand, Node<E> parent, Node<E> child) { super.afterRotate(grand, parent, child); //更新高度 updateHeight(grand);//g比较矮 updateHeight(parent);//p比较高 } //————————方式二:统一处理———————— /** * 恢复平衡(四种失衡情况一起处理) * @param node 高度最低的那个不平衡节点 */ private void rebalance2(Node<E> grand) { //p是g左右子树中高度较高的子树 Node<E> parent = ((AVLNode<E>)grand).tallerChild(); //n是p左右子树中高度较高的子树 Node<E> node = ((AVLNode<E>)parent).tallerChild(); if(parent.isLeftChild()) { //L if(node.isLeftChild()) { //LL rotate(grand,node.left,node,node.right, parent,parent.right,grand,grand.right); } else { //LR rotate(grand,parent.left,parent,node.left, node,node.right,grand,grand.right); } } else { //R if(node.isLeftChild()) { //RL rotate(grand,grand.left,grand,node.left, node,node.right,parent,parent.right); } else { //RR rotate(grand,grand.left,grand,parent.left, parent,node.left,node,node.right); } } } @Override protected void rotate(Node<E> r, Node<E> a, Node<E> b, Node<E> c, Node<E> d, Node<E> e, Node<E> f, Node<E> g) { super.rotate(r, a, b, c, d, e, f, g); //更新高度 updateHeight(b); updateHeight(f); updateHeight(d); } }

4.7 测试

public class AVLTreeTest { //添加删除测试 @Test public void test() { Integer[] data = new Integer[] { 67,52,92,96,53,95,13,63,34,82,76,54,9,68,39}; AVLTree<Integer> avl = new AVLTree<>(); for (int i = 0; i < data.length; i++) { avl.add(data[i]); } BinaryTrees.println(avl); for (int i = 0; i < data.length; i++) { avl.remove(data[i]); System.out.println("----------------------------"); System.out.println("【" + data[i] + "】"); BinaryTrees.println(avl); } } }

5. B树

5.1 理解

B树 是一种 平衡的多路搜索树,多用于文件系统、数据库的实现

clipboard (4)

仔细观察B树,有什么眼前一亮的特点?

  • 1 个节点可以存储超过 2 个元素、可以拥有超过 2 个子节点

  • 拥有二叉搜索树的一些性质

  • 平衡,每个节点的所有子树高度一致

  • 比较矮

数据库中一般使用 200 ~ 300 阶B树

5.2 m阶B树的性质

规定的B树必须要遵守的一些性质

假设一个节点存储的元素个数为 x

  • 根节点:1 ≤ x ≤ m − 1

  • 非根节点:┌ m/2 ┐ − 1 ≤ x ≤ m − 1 (┌ ┐ => 向上取整)

  • 如果有子节点,子节点个数 :y = x + 1

    • 根节点:2 ≤ y ≤ m

    • 非根节点:┌ m/2 ┐ ≤ y ≤ m

      ➢ 比如 m = 3,2 ≤ y ≤ 3,因此可以称为(2, 3)树、2-3树
      ➢ 比如 m = 4,2 ≤ y ≤ 4,因此可以称为(2, 4)树、2-3-4树
      ➢ 比如 m = 5,3 ≤ y ≤ 5,因此可以称为(3, 5)树
      ➢ 比如 m = 6,3 ≤ y ≤ 6,因此可以称为(3, 6)树
      ➢ 比如 m = 7,4 ≤ y ≤ 7,因此可以称为(4, 7)树

5.3 B树 与二叉搜索树 的关系

B树 和 二叉搜索树,在逻辑上是等价的

多代节点合并,可以获得一个 超级节点

  • 2代合并的超级节点,最多拥有 4 个子节点(至少是 4阶B树)

  • 3代合并的超级节点,最多拥有 8 个子节点(至少是 8阶B树)

  • n代合并的超级节点,最多拥有 2^n 个子节点( 至少是 2^n 阶B树)

m阶B树,最多需要 log2m 代合并

clipboard (5)

5.4 B树搜索

跟二叉搜索树的搜索类似

  • 先在节点内部从小到大开始搜索元素

  • 如果命中,搜索结束

  • 如果未命中,再去对应的子节点中搜索元素,重复步骤 1

clipboard (6)

5.5 B树添加

新添加的元素必定是添加到 叶子节点 中 √ 红黑树会用到这个结论clipboard

  • 插入55
    clipboard (1)
  • 插入98
clipboard (2)
  • 再插入 98 呢?(假设这是一棵 4阶B树)

    • 最右下角的叶子节点的元素个数将超过限制
    • 这种现象可以称之为:上溢(overflow)

添加 – 上溢的解决(假设5阶)

  • 上溢节点的元素个数必然等于 m

  • 假设上溢节点最中间元素的位置为 k

    • 将 k 位置的元素向上与父节点合并

    • 将 [0, k-1] 和 [k + 1, m - 1] 位置的元素分裂成 2 个子节点

      • 这 2 个子节点的元素个数,必然都不会低于最低限制(┌ m/2 ┐ − 1)
  • 一次分裂完毕后,有可能导致父节点上溢,依然按照上述方法解决

    • 最极端的情况,有可能一直分裂到根节点。如果一直传播到根节点就会导致B树变高(仅此一种情况导致B树变高)
clipboard (3)

插入98

clipboard

插入52

clipboard (1)

插入54

clipboard (2)

5.6 B树删除

如果需要删除的元素在 叶子节点 中,那么直接删除即可

clipboard

如果需要删除的元素在 非叶子节点 中

  • 先找到前驱或后继元素,覆盖所需删除元素的值
  • 再把前驱或后继元素删除
clipboard (1)
  • 非叶子节点 的前驱或后继元素,必定在 叶子节点
    • 所以这里的删除前驱或后继元素 ,就是最开始提到的情况:删除的元素在叶子节点中
    • 真正的删除元素都是发生在叶子节点中 √红黑树会用到这个结论

删除-非叶子节点的 下溢 现象

  • 删除 22 ?(假设这是一棵 5阶B树)

    • 叶子节点被删掉一个元素后,元素个数可能会低于最低限制( 即┌ m/2 ┐−1 )
    • 这种现象称为:**下溢(underflow)**
clipboard (2)

删除-非叶子节点的 下溢 解决

  • 下溢节点的元素数量必然等于 **┌ m/2 ┐ − 2**

  • 如果下溢节点临近的兄弟节点,有至少 **┌ m/2 ┐** 个元素,可以向其借一个元素

    • 将父节点的元素 **b** 插入到下溢节点的 **0** 位置(最小位置)
    • 用兄弟节点的元素 **a**(最大的元素)替代父节点的元素 b
    • 这种操作其实就是:**旋转**

注意:因为 b > a,所以不能破环二叉搜索树的性质直接将a放到下溢节点去。

clipboard (5)
  • 如果下溢节点临近的兄弟节点,只有 **┌ m/2 ┐ − 1** 个元素
  • 将父节点的元素 b 挪下来跟左右子节点进行合并
  • 合并后的节点元素个数等于**┌ m/2 ┐ + ┌ m/2 ┐ − 2**,不会超过 **m − 1** 上溢
  • 这个操作可能会导致父节点下溢,依然按照上述方法解决,下溢现象可能会一直往上传播。如果一直传播到根节点就会导致B树变矮(仅此一种情况导致B树变矮)
clipboard (6)

5.7 理解4阶b树

"理解了4阶b树,将能更好的学习理解 红黑树"

4阶B树的性质

  • 所有节点能存储的元素个数 x :1 ≤ x ≤ 3
  • 所有非叶子节点的子节点个数 y :2 ≤ y ≤ 4

添加

  • 手绘 从 1 添加到 22

删除

  • 手绘 从 1 删除到 22

6. 红黑树

6.1 理解

红黑树也是一种 自平衡的二叉搜索树,以前也叫做平衡二叉B树(Symmetric Binary B-tree)。

**红黑树必须满足以下 5 条性质 **

  • 节点是 RED 或者 BLACK

  • 根节点是 BLACK

  • 叶子节点(外部节点,空节点)都是 BLACK

  • RED 节点的子节点都是 `BLACK

    • RED 节点的 parent 都是 BLACK

    • 从根节点到叶子节点的所有路径上不能有 2 个连续的 RED 节点

  • 从任一节点到叶子节点的所有路径都包含 相同数目BLACK 节点

clipboard

注意:红黑树的 叶子节点 是让原来度为 0 的节点或度为 1 的节点都变成度为 2 的节点后的叶子节点。(增加空节点 null 实现此功能)此时红黑树就变成了真二叉树。

clipboard (1)

注意:之后展示的红黑树都会省略 null 节点 (空节点是假想出来的)

红黑树的平衡 (为什么满足以上5条性质,就能保证红黑树是平衡的?)

  • 以上5条性质,可以保证 红黑树 等价于 4阶B树

  • 相比AVL树,红黑树的平衡标准比较宽松:没有一条路径会大于其他路径的2倍

  • 可以理解为是一种弱平衡、黑高度平衡 (任意一条路的黑节点数量都是相等的)

  • 红黑树的最大高度是 2 ∗ log(n + 1) ,依然是 O(logn) 级别

红黑树的平均时间复杂度

  • 搜索:O(logn)

  • 添加:O(logn),O(1) 次的旋转操作

  • 删除:O(logn),O(1) 次的旋转操作

AVL树 对比 红黑树

  • AVL树

    • 平衡标准比较严格:每个左右子树的高度差不超过1

    • 最大高度是 1.44 ∗ log(n + 2) − 1.328(100W个节点,AVL树最大树高28)

    • 搜索、添加、删除都是 O(logn) 复杂度,其中添加仅需 O(1) 次旋转调整、删除最多需要 O(logn) 次旋转调整

  • 红黑树

    • 平衡标准比较宽松:没有一条路径会大于其他路径的2倍

    • 最大高度是 2 ∗ log(n + 1)( 100W个节点,红黑树最大树高40)

    • 搜索、添加、删除都是 O(logn) 复杂度,其中添加、删除都仅需 O(1) 次旋转调整

  • 搜索的次数远远大于插入和删除,选择AVL树;搜索、插入、删除次数几乎差不多,选择红黑树

  • 相对于AVL树来说,红黑树牺牲了部分平衡性以换取插入/删除操作时少量的旋转操作,整体来说性能要优于AVL树

  • 红黑树的平均统计性能优于AVL树,实际应用中更多选择使用红黑树

6.2 红黑树的等价变换

红黑树 和 四阶B树(2-3-4树)具有等价性

BLACK 节点与它的 RED 子节点融合在一起,形成1个B树节点

红黑树的 BLACK 节点个数 与 4阶B树的节点总个数 相等

注意:用 2-3树 与 红黑树 进行类比,这是极其不严谨的,2-3树 并不能完美匹配 红 黑树 的所有情况

clipboard (2)

6.3 红黑树 与 2-3-4树 的比较

如果下图最底层的 BLACK 节点是不存在的,在B树中是什么样的情形?=>整棵B树只有1个节点,而且是超级节点

clipboard (3)

6.4 添加节点

已知:

  • B树中,新元素必定是添加到叶子节点中

  • 4阶B树所有节点的元素个数 x 都符合 1 ≤ x ≤ 3

注意:

① 建议新添加的节点默认为 RED,这样能够让红黑树的性质尽快满足(性质1,2,3,5 都满足,性质 4 不一定)

② 如果添加的是根节点,染成 BLACK 即可

添加的所有情况

clipboard (4)

有 4 种情况既满足红黑树的性质四:parent 为 BLACK,同时也满足4阶B树的性质,因此不用做任何额外的处理。

clipboard (5)

有 8 种情况不满足红黑树的性质四:parent 为 RED( Double Red ),其中前 4 种属于B树节点上溢的情况

clipboard (6)

添加 – 修复性质4 – LL\RR

判定条件:uncle 不是 RED

  • parent 染成 BLACK,grand 染成 RED

  • grand 进行单旋操作:

    • LL:右旋转
    • RR:左旋转
clipboard

添加 – 修复性质4 – LR\RL

判定条件:uncle 不是 RED

  • 自己染成 BLACK,grand 染成 RED

  • 进行双旋操作:

    • LR:parent 左旋转, grand 右旋转
    • RL:parent 右旋转, grand 左旋转
clipboard (1)

添加 – 修复性质4 – 上溢 – LL

注意:之前修复的四种情况,添加节点的叔父节点都为null(null默认记为黑色)。

判定条件:uncle 是 RED

  • parent、uncle 染成 BLACK
  • grand 向上合并,且染成 RED,当做是新添加的节点进行处理

grand 向上合并时,可能继续发生上溢

若上溢持续到根节点,只需将根节点染成 BLACK

clipboard (2)

添加 – 修复性质4 – 上溢 – RR

判定条件:uncle 是 RED

  • parent、uncle 染成 BLACK
  • grand 向上合并,且染成 RED,当做是新添加的节点进行处理
clipboard (3)

添加 – 修复性质4 – 上溢 – LR

判定条件:uncle 是 RED

  • parent、uncle 染成 BLACK
  • grand 向上合并,且染成 RED,当做是新添加的节点进行处理
clipboard (4)

添加 – 修复性质4 – 上溢 – RL

判定条件:uncle 是 RED

  • parent、uncle 染成 BLACK
  • grand 向上合并,且染成 RED,当做是新添加的节点进行处理
clipboard (5)

6.4 删除节点

已知:B树中,最后真正被删除的元素都在叶子节点中

删除-RED节点

直接删除,不用做任何调整

clipboard

删除 – BLACK 节点 (有 3 种情况)

删除拥有 2 个 RED 子节点的 BLACK 节点 (如 25)

  • 不可能被直接删除,因为会找它的前驱节点或后继节点替代删除,在BSTree中已经实现了此功能因此也不用考虑这种情况

删除拥有 1 个 RED 子节点的 BLACK 节点 (如 46,76)

删除 BLACK 叶子节点 (如 88)

总结:删除后真正需要处理的只有两种情况:① 删除拥有 1 个 RED 子节点的 BLACK 节点 ② 删除 BLACK 叶子节点

删除 - 拥有 1 个 RED 子节点的 BLACK 节点

判定条件:用以替代的子节点是 RED “注意:删除Black叶子节点,没有用于替代的就相当于用null(默认为Black)替代”

将替代的子节点染成 BLACK 即可保持红黑树性质

clipboard (1)

删除 - BLACK 叶子节点 - sibling为 BLACK

BLACK 叶子节点被删除后,会导致B树节点下溢(比如删除88)

判定条件:如果 sibling 至少有 1 个 RED 子节点

  • 进行旋转操作
  • 旋转之后的中心节点继承 parent 的颜色
  • 旋转之后的左右节点染为 BLACK
clipboard (2)

判定条件:如果 sibling 没有 RED 子节点

  • 将 sibling 染成 RED、parent 染成 BLACK 即可修复红黑树性质 (合并)
  • 如果 parent 是 BLACK 会导致 parent 也下溢,这时只需要把 parent 当做被删除的节点处理即可(递归)
clipboard (3)

删除 - BLACK 叶子节点 - sibling为 RED

如果 sibling 是 RED

  • sibling 染成 BLACK,parent 染成 RED,进行旋转
  • 于是又回到 sibling 是 *BLACK 的情况
clipboard (4)

6.5 实现

/** * @Description 红黑树 * @author Polaris * @version * @date 2020年3月10日下午8:35:16 */ public class RBTree<E> extends BBSTree<E>{ private static final boolean RED = false; private static final boolean BLACK = true; public RBTree() { this(null); } public RBTree(Comparator<E> comparator) { super(comparator); } /** * RB树特有的节点 */ private static class RBNode<E> extends Node<E> { boolean color = RED; public RBNode(E element, Node<E> parent) { super(element, parent); } @Override public String toString() { String str = ""; if(color == RED) { str = "R_"; } return str + element.toString(); } } @Override protected Node<E> createNode(E element, Node<E> parent) { return new RBNode<E>(element,parent); } /** * 给节点上色 */ private Node<E> color(Node<E> node,boolean color) { if(node == null) return node; ((RBNode<E>)node).color = color; return node; } /** * 将节点染成红色 */ private Node<E> red(Node<E> node){ return color(node,RED); } /** * 将节点染成黑色 */ private Node<E> black(Node<E> node){ return color(node,BLACK); } /** * 获取当前节点的颜色 */ private boolean colorOf(Node<E> node) { return node == null ? BLACK : ((RBNode<E>)node).color; } /** * 判断当前颜色是否为黑色 */ private boolean isBlack(Node<E> node) { return colorOf(node) == BLACK; } /** * 判断当前颜色是否为红色 */ private boolean isRed(Node<E> node) { return colorOf(node) == RED; } /** * 实现添加新节点后的处理操作 */ @Override protected void afterAdd(Node<E> node) { Node<E> parent = node.parent; //添加的是根节点 或 上溢到根节点 if(parent == null) { black(node); return; } //类型一:parent是黑色(不用处理四种情况) if(isBlack(parent)) return; //类型二:parent是红色且uncle是红色(会上溢的四种情况) Node<E> uncle = parent.getSibling(); Node<E> grand = red(parent.parent);//以下情况都需要将grand染成红色,可以统一处理 if(isRed(uncle)) { black(parent); black(uncle); //把祖父节点当作是新添加的节点 afterAdd(grand);//上溢递归调用 return; } //类型三:parent是红色且uncle不是红色(需要旋转的四种情况) if(parent.isLeftChild()) {//L if(node.isLeftChild()) { //LL black(parent); } else { //LR black(node); rotateLeft(parent); } rotateRight(grand); } else { //R if(node.isLeftChild()) { //RL black(node); rotateRight(parent); } else { //RR black(parent); } rotateLeft(grand); } } /** * 实现删除节点后的处理操作 */ @Override protected void afterRemove(Node<E> node,Node<E> replacement) { //情况一:如果删除的节点是红色,不用处理 if(isRed(node)) return; //情况二:用于取代node子节点的是红色节点 if(isRed(replacement)) { black(replacement); return; } //情况三:删除的是黑色叶子节点(下溢) Node<E> parent = node.parent; //删除的是根节点 if(parent == null) return; //判断被删除的node的节点是左还是右 boolean left = parent.left == null || node.isLeftChild(); Node<E> sibling = left ? parent.right : parent.left; if(left) { //被删除的节点在左边,兄弟节点在右边(镜像对称处理) if(isRed(sibling)) { //兄弟节点是红色,就要转成黑色 black(sibling); red(parent); rotateLeft(parent); //更换兄弟 sibling = parent.right; } //兄弟节点必然是黑色 if(isBlack(sibling.left) && isBlack(sibling.right)) { //兄弟节点没有一个红色子节点,父节点要向下向子节点合并 boolean parentBlack = isBlack(parent); black(parent); red(sibling); if(parentBlack) { afterRemove(parent, null); } } else { //兄弟节点至少有 1 个红色节点,就要向兄弟节点借元素 if(isBlack(sibling.right)) { //兄弟节点的右边不是红色,则兄弟要先旋转 rotateRight(sibling); sibling = parent.right; } color(sibling,colorOf(parent)); black(sibling.right); black(parent); rotateLeft(parent); } } else { //被删除的节点在右边,兄弟节点在左边(图示的是这种) if(isRed(sibling)) { //兄弟节点是红色,就要转成黑色 black(sibling); red(parent); rotateRight(parent); //更换兄弟 sibling = parent.left; } //兄弟节点必然是黑色 if(isBlack(sibling.left) && isBlack(sibling.right)) { //兄弟节点没有一个红色子节点,父节点要向下向子节点合并 boolean parentBlack = isBlack(parent); black(parent); red(sibling); if(parentBlack) { afterRemove(parent, null); } } else { //兄弟节点至少有 1 个红色节点,就要向兄弟节点借元素 if(isBlack(sibling.left)) { //兄弟节点的左边不是红色,则兄弟要先旋转 rotateLeft(sibling); sibling = parent.left; } color(sibling,colorOf(parent)); black(sibling.left); black(parent); rotateRight(parent); } } } }

6.6 测试

public class RBTreeTest { //添加测试 @Test public void test() { Integer[] data = new Integer[] { 55,87,56,74,96,22,62,20,70,68,90,50}; RBTree<Integer> rb = new RBTree<>(); for (int i = 0; i < data.length; i++) { rb.add(data[i]); System.out.println("----------------------------"); System.out.println("【" + data[i] + "】"); BinaryTrees.println(rb); } BinaryTrees.println(rb); } //删除测试 @Test public void test1() { Integer[] data = new Integer[] { 55,87,56,74,96,22,62,20,70,68,90,50}; RBTree<Integer> rb = new RBTree<>(); for (int i = 0; i < data.length; i++) { rb.add(data[i]); } BinaryTrees.println(rb); for (int i = 0; i < data.length; i++) { rb.remove(data[i]); System.out.println("----------------------------"); System.out.println("【" + data[i] + "】"); BinaryTrees.println(rb); } } }

六. 集合(Set)实现

1. 集合的特点

不存放重复的元素

常用于去重

  • 存放新增 IP,统计新增 IP 量

  • 存放词汇,统计词汇量

集合的内部实现能直接使用 动态数组链表二叉搜索树(AVL树,红黑树) 实现

2. 接口设计

/** * @Description 集合Set的接口 * @author Polaris * @version * @date 2020年3月11日下午9:30:36 */ public interface Set<E> { int size(); boolean isEmpty(); void clear(); boolean contains(E element); void add(E element); void remove(E element); void traversal(Visitor<E> visitor); /** * 注意:动态数组或链表有索引的概念能直接for循环遍历,不需要遍历接口 */ public static abstract class Visitor<E> { boolean stop; public abstract boolean visit(E element); } }

3. 链表实现集合(ListSet)

3.1 实现

public class ListSet<E> implements Set<E> { private List<E> list = new LinkedList<E>(); @Override public int size() { return list.size(); } @Override public boolean isEmpty() { return list.isEmpty(); } @Override public void clear() { list.clear(); } @Override public boolean contains(E element) { return list.contains(element); } @Override public void add(E element) { //集合Set存储不重复的元素 if(list.contains(element)) return; list.add(element); } @Override public void remove(E element) { int index = list.indexOf(element); if(index != -1) { list.remove(index); } } @Override public void traversal(Visitor<E> visitor) { if(visitor == null) return; int size = list.size(); for(int i = 0;i < size;i++) { if(visitor.visit(list.get(i))) return; } } }

3.2 测试

public class ListSetTest { @Test public void test() { Set<Integer> listSet = new ListSet<>(); listSet.add(10); listSet.add(11); listSet.add(11); listSet.add(12); listSet.add(7); listSet.remove(11); listSet.traversal(new Visitor<Integer>() { @Override public boolean visit(Integer element) { System.out.println(element); return false; } }); } }

4. 红黑树实现集合(TreeSet)

4.1 ListSet 与 TreeSet效率对比

链表

  • 查找:最坏情况为O(n)级别

  • 添加:最坏情况为O(n)级别

  • 删除:最坏情况为O(n)级别

红黑树

  • 查找:最坏情况为O(logn)级别

  • 添加:最坏情况为O(logn)级别

  • 删除:最坏情况为O(logn)级别

4.2 TreeSet 的局限性

通过二叉搜索树实现的TreeSet,元素必须具备 可比较性 才能加进去

通过 哈希表 实现的 HashSet,可以解决这个局限性

4.3 实现

public class TreeSet<E> implements Set<E>{ private RBTree<E> tree; public TreeSet() { this(null); } public TreeSet(Comparator<E> comparator) { tree = new RBTree<>(comparator); } @Override public int size() { return tree.size(); } @Override public boolean isEmpty() { return tree.isEmpty(); } @Override public void clear() { tree.clear(); } @Override public boolean contains(E element) { return tree.contains(element); } @Override public void add(E element) { tree.add(element); } @Override public void remove(E element) { tree.remove(element); } @Override public void traversal(Visitor<E> visitor) { tree.inorderTraversal(new BinaryTree.Visitor<E>() { @Override public boolean visit(E element) { return visitor.visit(element); } }); } }

4.4 测试

public class TreeSetTest { @Test public void test() { Set<Person> treeSet = new TreeSet<>(new Comparator<Person>() { @Override public int compare(Person o1, Person o2) { Person p1 = (Person)o1; Person p2 = (Person)o2; return p1.getAge() - p2.getAge(); } }); treeSet.add(new Person(10)); treeSet.add(new Person(12)); treeSet.add(new Person(10)); treeSet.add(new Person(7)); treeSet.traversal(new Visitor<Person>() { @Override public boolean visit(Person element) { System.out.println(element.getAge()); return false; } }); } } class Person { private int age; public Person(int age) { super(); this.age = age; } public int getAge() { return age; } public void setAge(int age) { this.age = age; } }

七.映射(Map)实现

1. 理解

Map 在有些编程语言中也叫做字典(dictionary,比如 Python、Objective-C、Swift 等)

clipboard (5)

Map 的每一个 key 是唯一的

类似Set,Map可以直接利用链表二叉搜索树 (AVL树,红黑树)等数据结构来实现

2. Map 与 Set 的关系

Map 的所有 key 组合在一起,其实就是一个 Set。因此,Set 可以间接利用 Map 来作内部实现

3. 接口设计

public interface Map<K, V> { int size(); boolean isEmpty(); void clear(); V put(K key,V value); V get(K key); V remove(K key); boolean containsKey(K key); boolean containsValue(V value); void traversal(Visitor<K,V> visitor); public static abstract class Visitor<K,V> { boolean stop; public abstract boolean visit(K key,V value); } }

4. 红黑树实现TreeMap

4.1 TreeMap分析

时间复杂度(平均)

  • 添加、删除、搜索:O(logn)

特点

  • Key 必须具备可比较性

  • 元素的分布是有顺序的

在实际应用中,很多时候的需求

  • Map 中存储的元素不需要讲究顺序

  • Map 中的 Key 不需要具备可比较性

不考虑顺序、不考虑 Key 的可比较性,Map 有更好的实现方案,平均时间复杂度可以达到 O(1),那就是采取哈希表来实现 Map

4.2 实现

/** * @Description 红黑树实现映射(把TreeMap本身当成一棵红黑树, * 用key和value代替element,即从头开始用红黑树实现一个Map) * @author Polaris * @version * @date 2020年3月12日下午6:25:57 */ @SuppressWarnings({"unchecked","unused"}) public class TreeMap<K,V> implements Map<K,V>{ private static final boolean RED = false; private static final boolean BLACK = true; protected int size; protected Node<K,V> root;// 根节点 protected Comparator<K> comparator;// 比较器定制排序 public TreeMap() { this(null); } public TreeMap(Comparator<K> comparator) { this.comparator = comparator; } private static class Node<K,V> { K key; V value; boolean color = RED; Node<K,V> left; // 左子节点 Node<K,V> right; // 右子节点 Node<K,V> parent; // 父节点 public Node(K key,V value, Node<K,V> parent) { this.key = key; this.value = value; this.parent = parent; } public boolean isLeaf() { return left == null && right == null; } public boolean hasTwoChildren() { return left != null && right != null; } public boolean isLeftChild() { return parent != null && this == parent.left; } public boolean isRightChild() { return parent != null && this == parent.right; } public Node<K,V> getSibling(){ if(isLeftChild()) { return parent.right; } if(isRightChild()) { return parent.left; } return null; } } /** * 检查key是否为空 */ protected void keyNoNullCheck(K key) { if (key == null) { throw new IllegalArgumentException("key must no be null!"); } } @Override public int size() { return size; } @Override public boolean isEmpty() { return size == 0; } @Override public void clear() { root = null; size = 0; } @Override public V put(K key, V value) { //一般只要求key具有可比较性就 keyNoNullCheck(key); //添加第一个节点(根节点) if (root == null) { root = new Node<>(key, value, null); size++; afterPut(root);//新添加节点之后的处理 return null; } // 如果添加的不是第一个节点: // 1.找到待添加位置的父节点 Node<K,V> parent = root; Node<K,V> node = root; int cmp = 0; while(node != null) { cmp = compare(key, node.key); parent = node; if (cmp > 0) { node = node.right; } else if (cmp < 0) { node = node.left; } else { node.key = key;//一般覆盖(不同对象可能有相同的比较参数) V oldValue = node.value; node.value = value; return oldValue; } } // 2.判断插入父节点的左子节点还是右子节点 Node<K,V> newNode = new Node<>(key, value, parent); if (cmp > 0) { parent.right = newNode; } else { parent.left = newNode; } size++; afterPut(newNode);//新添加节点之后的处理 return null; } private void afterPut(Node<K,V> node) { Node<K,V> parent = node.parent; //添加的是根节点 或 上溢到根节点 if(parent == null) { black(node); return; } //类型一:parent是黑色(不用处理四种情况) if(isBlack(parent)) return; //类型二:parent是红色且uncle是红色(会上溢的四种情况) Node<K,V> uncle = parent.getSibling(); Node<K,V> grand = red(parent.parent);//以下情况都需要将grand染成红色,可以统一处理 if(isRed(uncle)) { black(parent); black(uncle); //把祖父节点当作是新添加的节点 afterPut(grand);//上溢递归调用 return; } //类型三:parent是红色且uncle不是红色(需要旋转的四种情况) if(parent.isLeftChild()) {//L if(node.isLeftChild()) { //LL black(parent); } else { //LR black(node); rotateLeft(parent); } rotateRight(grand); } else { //R if(node.isLeftChild()) { //RL black(node); rotateRight(parent); } else { //RR black(parent); } rotateLeft(grand); } } private int compare(K k1, K k2) { if (comparator != null) { return comparator.compare(k1, k2); } return ((Comparable<K>)k1).compareTo(k2); } /** * 给节点上色 */ private Node<K,V> color(Node<K,V> node,boolean color) { if(node == null) return node; node.color = color; return node; } /** * 将节点染成红色 */ private Node<K,V> red(Node<K,V> node){ return color(node,RED); } /** * 将节点染成黑色 */ private Node<K,V> black(Node<K,V> node){ return color(node,BLACK); } /** * 获取当前节点的颜色 */ private boolean colorOf(Node<K,V> node) { return node == null ? BLACK : node.color; } /** * 判断当前颜色是否为黑色 */ private boolean isBlack(Node<K,V> node) { return colorOf(node) == BLACK; } /** * 判断当前颜色是否为红色 */ private boolean isRed(Node<K,V> node) { return colorOf(node) == RED; } /** * 左旋转,以RR为例 */ private void rotateLeft(Node<K,V> grand) { Node<K,V> parent = grand.right; Node<K,V> child = parent.left;//child就是T1子树 grand.right = child; parent.left = grand; afterRotate(grand, parent, child); } /** * 右旋转,以LL为例 */ private void rotateRight(Node<K,V> grand) { Node<K,V> parent = grand.left; Node<K,V> child = parent.right; grand.left = child; parent.right = grand; afterRotate(grand, parent, child); } /** * 抽取左旋转和右旋转中的重复代码 */ private void afterRotate(Node<K,V> grand,Node<K,V> parent,Node<K,V> child) { //更新parent的parent(让parent成为子树的根节点) parent.parent = grand.parent; if(grand.isLeftChild()) { grand.parent.left = parent; } else if(grand.isRightChild()) { grand.parent.right = parent; } else { //grand是root节点 root = parent; } //更新child的parent if(child != null) { child.parent = grand; } //更新grand的parent grand.parent = parent; } @Override public V get(K key) { Node<K,V> node = node(key); return node != null ? node.value : null; } /** * 根据key找到对应节点 */ private Node<K,V> node(K key){ Node<K,V> node = root; while(node != null) { int cmp = compare(key,node.key); if(cmp == 0) return node; if(cmp > 0) { node = node.right; } else { node = node.left; } } return null; } @Override public V remove(K key) { return remove(node(key)); } /** * 根据key删除节点元素 */ private V remove(Node<K,V> node) { if(node == null) return null; size--; V oldValue = node.value; //考虑度为2的节点,转化为度为1 if(node.hasTwoChildren()) { Node<K,V> s = successor(node);//后继节点 //用后继节点的值覆盖度为2的节点的值 node.key = s.key; node.value = s.value; //删除后继节点 node = s; } //删除node节点(能到这则说明node的度必为0或1) Node<K,V> replacement = node.left != null ? node.left : node.right; if(replacement != null) { //node是度为1的节点 //更改parent replacement.parent = node.parent; //更改parent的left,right指向 if(node.parent == null) { //node是度为1的节点也是根节点 root = replacement; } else if(node == node.parent.left) { node.parent.left = replacement; } else { //在右边 node.parent.right = replacement; } //此时开始恢复平衡(AVL树 或 RB树需要实现此方法) afterRemove(node,replacement); } else if(node.parent == null){ //node是叶子节点也是根节点 root = null; afterRemove(node,null); } else { //node是叶子节点但不是根节点 if(node == node.parent.left) { node.parent.left = null; } else { node.parent.right = null; } //此时开始恢复平衡(AVL树 或RB树 需要实现此方法) afterRemove(node,null); } return oldValue; } /** * 实现删除节点后的处理操作 */ private void afterRemove(Node<K,V> node,Node<K,V> replacement) { //情况一:如果删除的节点是红色,不用处理 if(isRed(node)) return; //情况二:用于取代node子节点的是红色节点 if(isRed(replacement)) { black(replacement); return; } //情况三:删除的是黑色叶子节点(下溢) Node<K,V> parent = node.parent; //删除的是根节点 if(parent == null) return; //判断被删除的node的节点是左还是右 boolean left = parent.left == null || node.isLeftChild(); Node<K,V> sibling = left ? parent.right : parent.left; if(left) { //被删除的节点在左边,兄弟节点在右边(镜像对称处理) if(isRed(sibling)) { //兄弟节点是红色,就要转成黑色 black(sibling); red(parent); rotateLeft(parent); //更换兄弟 sibling = parent.right; } //兄弟节点必然是黑色 if(isBlack(sibling.left) && isBlack(sibling.right)) { //兄弟节点没有一个红色子节点,父节点要向下向子节点合并 boolean parentBlack = isBlack(parent); black(parent); red(sibling); if(parentBlack) { afterRemove(parent, null); } } else { //兄弟节点至少有 1 个红色节点,就要向兄弟节点借元素 if(isBlack(sibling.right)) { //兄弟节点的右边不是红色,则兄弟要先旋转 rotateRight(sibling); sibling = parent.right; } color(sibling,colorOf(parent)); black(sibling.right); black(parent); rotateLeft(parent); } } else { //被删除的节点在右边,兄弟节点在左边(图示的是这种) if(isRed(sibling)) { //兄弟节点是红色,就要转成黑色 black(sibling); red(parent); rotateRight(parent); //更换兄弟 sibling = parent.left; } //兄弟节点必然是黑色 if(isBlack(sibling.left) && isBlack(sibling.right)) { //兄弟节点没有一个红色子节点,父节点要向下向子节点合并 boolean parentBlack = isBlack(parent); black(parent); red(sibling); if(parentBlack) { afterRemove(parent, null); } } else { //兄弟节点至少有 1 个红色节点,就要向兄弟节点借元素 if(isBlack(sibling.left)) { //兄弟节点的左边不是红色,则兄弟要先旋转 rotateLeft(sibling); sibling = parent.left; } color(sibling,colorOf(parent)); black(sibling.left); black(parent); rotateRight(parent); } } } /** * 利用中序遍历求某个节点的前驱节点 */ private Node<K,V> predecessor(Node<K,V> node) { if(node == null) return null; //前驱节点在左子树中:node.left.right.right... Node<K,V> p = node.left; if(p != null) { while(p.right != null) { p = p.right; } return p; } //从祖父节点中寻找前驱节点 while(node.parent != null && node == node.parent.left) { node = node.parent; } //情况一:node.parent == null ↓ //情况二:node == node.parent.right ↓ return node.parent; } /** * 利用中序遍历求某个节点的后继节点 */ private Node<K,V> successor(Node<K,V> node) { if(node == null) return null; //前驱节点在右子树中:node.right.left.left... Node<K,V> p = node.right; if(p != null) { while(p.left != null) { p = p.left; } return p; } //从祖父节点中寻找前驱节点 while(node.parent != null && node == node.parent.right) { node = node.parent; } //情况一:node.parent == null ↓ //情况二:node == node.parent.left ↓ return node.parent; } @Override public boolean containsKey(K key) { return node(key) != null; } @Override public boolean containsValue(V value) { if(root == null) return false; Queue<Node<K,V>> queue = new LinkedList<>(); queue.offer(root); while(!queue.isEmpty()) { Node<K,V> node = queue.poll(); if(valEquals(value, node.value)) return true; if(node.left != null) { queue.offer(node.left); } if(node.right != null) { queue.offer(node.right); } } return false; } private boolean valEquals(V v1,V v2) { return v1 == null ? v2 == null : v1.equals(v2); } @Override public void traversal(Visitor<K, V> visitor) { if(visitor == null) return; traversal(root,visitor); } private void traversal(Node<K,V> node,Visitor<K, V> visitor) { if(node == null || visitor.stop) return; traversal(node.left,visitor); if(visitor.stop) return; visitor.visit(node.key, node.value); traversal(node.right,visitor); } }

4.3 测试

public class TreeMapTest { @Test public void test() { Map<String,Integer> map = new TreeMap<>(); map.put("c", 2); map.put("a", 5); map.put("b", 6); map.put("a", 8); map.traversal(new Visitor<String, Integer>() { @Override public boolean visit(String key, Integer value) { System.out.println(key + "_" + value); return false; } }); } @Test public void test2() { FileInfo fileInfo = Files.read("D:\\Learning\\Java" + "\\workspace_eclipse\\workspace001_2019-3" + "\\DataStructures\\src\\com\\polaris4" + "\\map", new String[]{"java"}); System.out.println("文件数量:" + fileInfo.getFiles()); System.out.println("代码行数:" + fileInfo.getLines()); String[] words = fileInfo.words(); System.out.println("单词数量:" + words.length); Map<String, Integer> map = new TreeMap<>(); for (int i = 0; i < words.length; i++) { Integer count = map.get(words[i]); count = (count == null) ? 1 : (count + 1); map.put(words[i], count); } map.traversal(new Visitor<String, Integer>() { public boolean visit(String key, Integer value) { System.out.println(key + "_" + value); return false; } }); } }
public class Files { /** * 读取文件内容 * @param file * @return */ public static FileInfo read(String file) { if (file == null) return null; FileInfo info = new FileInfo(); StringBuilder sb = new StringBuilder(); try (FileReader reader = new FileReader(file); BufferedReader br = new BufferedReader(reader)) { String line; while ((line = br.readLine()) != null) { sb.append(line).append("\n"); info.setLines(info.getLines() + 1); } int len = sb.length(); if (len > 0) { sb.deleteCharAt(len - 1); } } catch (IOException e) { e.printStackTrace(); } info.setFiles(info.getFiles() + 1); info.setContent(sb.toString()); return info; } /** * 读取文件夹下面的文件内容 * @param dir * @param extensions * @return */ public static FileInfo read(String dir, String[] extensions) { if (dir == null) return null; File dirFile = new File(dir); if (!dirFile.exists()) return null; FileInfo info = new FileInfo(); dirFile.listFiles(new FileFilter() { public boolean accept(File subFile) { String subFilepath = subFile.getAbsolutePath(); if (subFile.isDirectory()) { info.append(read(subFilepath, extensions)); } else if (extensions != null && extensions.length > 0) { for (String extension : extensions) { if (subFilepath.endsWith("." + extension)) { info.append(read(subFilepath)); break; } } } else { info.append(read(subFilepath)); } return false; } }); return info; } }
public class FileInfo { private int lines; private int files; private String content = ""; public String[] words() { return content.split("[^a-zA-Z]+"); } public int getFiles() { return files; } public void setFiles(int files) { this.files = files; } public int getLines() { return lines; } public void setLines(int lines) { this.lines = lines; } public String getContent() { return content; } public void setContent(String content) { this.content = content; } public FileInfo append(FileInfo info) { if (info != null && info.lines > 0) { this.files += info.files; this.lines += info.lines; this.content = new StringBuilder(this.content) .append("\n") .append(info.content) .toString(); } return this; } }

八.哈希表

1. 理解

哈希表也叫做 散列表( hash 有“剁碎”的意思)

它是如何实现高效处理数据的?

  • put("Jack", 666);

  • put("Rose", 777);

  • put("Kate", 888);

添加、搜索、删除的流程都是类似的

  • 利用哈希函数生成 key 对应的 index【O(1)】

  • 根据 index 操作定位数组元素【O(1)】

哈希表是【空间换时间】的典型应用

哈希函数,也叫做 散列函数

哈希表内部的数组元素,很多地方也叫 Bucket(桶),整个数组叫 Buckets 或者 Bucket Array

clipboard (6)

注意:在实际应用中很多时候的需求:Map 中存储的元素不需要讲究顺序,Map 中的 Key 不需要具备可比较性。其实不考虑顺序、不考虑 Key 的可比较性,Map 有更好的实现方案,平均时间复杂度可以达到 O(1) ,那就是采取 哈希表来实现 Map

2. 哈希冲突(Hash Collision)

哈希冲突也叫做 哈希碰撞

  • 2 个不同的 key,经过哈希函数计算出相同的结果
  • key1 ≠ key2 ,hash(key1) = hash(key2)
clipboard (7)

解决哈希冲突的常见方法

  • 开放定址法(Open Addressing)即按照一定规则向其他地址探测,直到遇到空桶 。

  • 再哈希法(Re-Hashing)即设计多个哈希函数

  • 链地址法(Separate Chaining) 即比如通过链表将同一index的元素串起来

JDK1.8的哈希冲突解决方案

  • 默认使用 单向链表 将元素串起来(链地址法
  • 在添加元素时,可能会由 单向链表 转为 红黑树 来存储元素。比如当哈希表容量 ≥ 64 且 单向链表的节点数量大于 8 时
  • 红黑树 节点数量少到一定程度时,又会转为 单向链表
  • JDK1.8中的哈希表是使用 链表+红黑树解决哈希冲突
  • 思考一下这里为什么使用单链表?=> 每次都是从头节点开始遍历,单向链表比双向链表少一个指针,可以节省内存空间
clipboard (8)

3. 哈希函数

哈希表中哈希函数的实现步骤大概如下:

  • 先生成 key 的哈希值(必须是整数

  • 再让 key 的哈希值数组的大小 进行相关运算,生成一个 索引值

public int hash(Object key) { return hash_code(key) % table.length; }

为了提高效率,可以使用 & 位运算取代 % 运算【前提:将数组的长度设计为 2 的幂(2^n)

public int hash(Object key) { return hash_code(key) & (table.length - 1); } // 1 = 2^0 // 10 = 2^1 // 100 = 2^2 // 1000 = 2^3 // 10000 = 2^4 // 01111 = 2^4 - 1 = 1111 // ==> table.length - 1 表示 & 一个全为1的二进制数。结果必然小与这个全为1的二进制数 // 假设哈希值为1001010 => // 1001010 // & 0001111 // ---------- // 0001010 => 生成的值的范围是 0000 ~ 1111

良好的哈希函数 能让哈希值更加均匀分布 → 减少哈希冲突次数 → 提升哈希表的性能

此外,hashCode相等,生成的索引不一定相等。

11000 &111 ------ 000 10000 &111 ------ 000

4. 如何生成key的哈希值

key 的常见种类可能有

  • 整数、浮点数、字符串、自定义对象

  • 不同种类的 key,哈希值的生成方式不一样,但目标是一致的

    • 尽量让每个 key 的哈希值是唯一的

    • 尽量让 key 的所有信息参与运算

在Java中,HashMap 的 key 必须实现 hashCodeequals 方法,也允许 key 为 null

整数

整数值当做哈希值

比如 10 的哈希值就是 10

public static int hashCode(int value) { return value; }

浮点数

将存储的二进制格式转为整数值

public static int hashCode(float value) { return Float.floatToIntBits(value); }

long

注意:Java的哈希值必须是 int 类型(32位)

public static int hashCode(long value) { //如果强制转换为int会直接砍掉前面32位,不推荐 return (int)(value ^ (value >>> 32)); } // 注意:>>> 和 ^ 的作用是?(>>>是无符号右移,^是异或运算) // ① 高32bit 和 低32bit 混合计算出 32bit 的哈希值 // ② 充分利用所有信息计算出哈希值 // 另外:为什么不用 & 或者 |而是用 ^ ? // ① 如果value前32位全为1,使用 & 运算后32位就相当于没算了。 // ② 如果value前32位全为1,使用 | 运算后32位就全为1了。
clipboard (9)

double

public static int hashCode(double value) { long bits = doubleToLongBits(value); return (int)bits ^ (bits >>> 32); }

字符串

先看一个问题:整数 5489 是如何计算出来的?

5 ∗ 10^3 + 4 ∗ 10^2 + 8 ∗ 10^1 + 9 ∗ 10^0

字符串是由若干个字符组成的

  • 比如字符串 jack,由 j、a、c、k 四个字符组成(字符的本质就是一个整数,ASCII码)
  • 因此,jack 的哈希值可以表示为 j ∗ n^3 + a ∗ n^2 + c ∗ n^1 + k ∗ n^0,等价于 [ ( j ∗ n + a ) ∗ n + c ] ∗ n + k (等价后可以避免n的重复计算)
  • 在JDK中,乘数 n 为 31,为什么使用 31? => 31 是一个奇素数,JVM会将 31 * i 自动优化转化为 (i << 5) – i

注意:

① 31 * i = (2^5 - 1) * i = i * 2^5 - i = (i << 5) - i

② 31不仅仅是符合2^n - 1,它也是一个奇素数(既是奇数,也是质数。即质数)

=>素数和其他数相乘的结果比其他方式更容易产生唯一性,减少哈希冲突。

@Test public void StrHashTest() { String str = "jack"; int len = str.length(); int hashCode = 0; for(int i = 0;i < len;i++) { char c = str.charAt(i); //hashCode = (hashCode << 5) - hashCode + c; hashCode = hashCode * 31 + c; // [ ( j ∗ n + a ) ∗ n + c ] ∗ n + k } System.out.println(hashCode);//3254239 System.out.println(str.hashCode());//3254239 }

总结

@Test public void hashTest() { int a = 110; float b = 10.6f; long c = 156l; double d = 10.9; String e = "rose"; System.out.println(Integer.hashCode(a)); System.out.println(Float.hashCode(b)); //System.out.println(Float.floatToIntBits(b)); //内部实现 System.out.println(Long.hashCode(c)); System.out.println(Double.hashCode(d)); System.out.println(e.hashCode()); }

自定义对象的哈希值

自定义对象的hash值默认与该对象的内存地址有关。

注意:

① 哈希值太大,整型溢出怎么办? => 不用作任何处理,溢出了还是一个整 数。

② 不重写hashCode方法有什么后果? => 会以对象内存地址相关的值作为hash值。

重点:

① hashCode方法在在计算索引时调用

② equals方法在hash冲突时比较两个key是否相等时调用

④ 如果要求两个对象的哪些成员变量相等就代表这两个对象相等的话,hashCode方法和equals方法就只包含这些成员变量的计算就可以了。(hashCode方法必须要保证 equals 为 true 的 2 个key的哈希值一样,反过来hashCode相等的key,不一定equals为true)

public class HashTest { @Test public void PersonHashTest() { Person p1 = new Person(15,"rose",58.5f); Person p2 = new Person(15,"rose",58.5f); //System.out.println(p1.hashCode());//1834188994 //System.out.println(p2.hashCode());//1174361318 //=>自定义对象hash值默认与对象的地址值有关 //√ 重写hashCode方法后:hash值相等意味着生成的索引相同 System.out.println(p1.hashCode());//185317790 System.out.println(p2.hashCode());//185317790 Map<Object,Object> map = new HashMap<>(); map.put(p1,"abc"); map.put(p2,"bcd");//如果p1与p2"相等",就会覆盖,此时size为1才合理。 //=>此时需要重写equals方法比较两个key是否"相等" //√ 注意:不能通过hash值的比较来判断两个key"相等",因为可能两个 // 完全不同类型的key的hash值是相等的。 System.out.println(map.size());//1 } } class Person { private int age; private String name; private float height; public Person(int age, String name, float height) { super(); this.age = age; this.name = name; this.height = height; } public Person() { super(); } public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public float getHeight() { return height; } public void setHeight(float height) { this.height = height; } @Override public String toString() { return "Person [age=" + age + ", name=" + name + ", height=" + height + "]"; } /** * 用来计算当前对象的hash值 */ @Override public int hashCode() { //Integer.hashCode(age); //Float.hashCode(height); //name != null ? name.hashCode() : 0; int hashCode = Integer.hashCode(age); hashCode = hashCode * 31 + Float.hashCode(height); hashCode = hashCode * 31 + (name != null ? name.hashCode() : 0); return hashCode; } /** * 用来比较两个对象是否相等 */ @Override public boolean equals(Object obj) { if(this == obj) return true; //if(obj == null || obj instanceof Person) return false; if(obj == null || obj.getClass() != getClass()) return false; Person p = (Person)obj; return p.age == age && p.height == height && p.name == null ? name == null : p.name.equals(name); } }

5. HashMap实现

这里有如下设计

  • 直接使用红黑树解决hash冲突
  • 数组元素存储红黑树根节点,而不是存储红黑树对象。这样做的好处是就不用额外存储红黑树的size,comparator属性了(用不上)。
/** * @Description hashMap, hash冲突直接使用红黑树解决 * @Author monap * @Date 2022/1/2 15:53 */ @SuppressWarnings("unchecked") public class HashMap<K, V> implements Map<K, V> { // 所有节点的数量 private int size; private static final boolean RED = false; private static final boolean BLACK = true; private Node<K, V>[] table; private static final int DEFAULT_CAPACTIY = 1 << 4; // 默认容量 private static final float DEFAULT_LOAD_FACTOR = 0.75f; // 装填因子 protected static class Node<K, V> { int keyHash; K key; V value; boolean color = RED; Node<K, V> left; // 左子节点 Node<K, V> right; // 右子节点 Node<K, V> parent; // 父节点 public Node(K key, V value, Node<K, V> parent) { this.key = key; int hash = key == null ? 0 : key.hashCode(); this.keyHash = hash ^ (hash >>> 16); // 扰动计算一次,使hash值排布更均匀 this.value = value; this.parent = parent; } public boolean isLeaf() { return left == null && right == null; } public boolean hasTwoChildren() { return left != null && right != null; } public boolean isLeftChild() { return parent != null && this == parent.left; } public boolean isRightChild() { return parent != null && this == parent.right; } public Node<K, V> getSibling() { if (isLeftChild()) { return parent.right; } if (isRightChild()) { return parent.left; } return null; } @Override public String toString() { return "Node_" + key + "_" + value; } } protected Node<K, V> createNode(K key, V value, Node<K, V> parent) { return new Node<>(key, value, parent); } /** * 获取key的hash值,并做一次扰动计算 * * @param key * @return */ private int hash(K key) { if (key == null) return 0; int hash = key.hashCode(); return hash ^ (hash >>> 16); } /** * 根据key生成索引(在桶数组中的位置) * * @param key * @return */ private int index(K key) { return hash(key) & (table.length - 1); } /** * 根据node获取索引(在桶数组中的位置) * * @param node * @return */ private int index(Node<K, V> node) { return node.keyHash & (table.length - 1); } public HashMap() { table = new Node[DEFAULT_CAPACTIY]; } @Override public int size() { return size; } @Override public boolean isEmpty() { return size == 0; } @Override public void clear() { if (size == 0) return; size = 0; Arrays.fill(table, null); } @Override public V put(K key, V value) { // 检查是否需要扩容 resize(); int index = index(key); // 取出index位置的红黑树根节点 Node<K, V> root = table[index]; if (root == null) { root = createNode(key, value, null); table[index] = root; size++; afterPut(root); return null; } // hash冲突,添加新节点到红黑树 // 1.找到待添加位置的父节点 Node<K, V> parent = root; Node<K, V> node = root; int cmp = 0; K k1 = key; int h1 = hash(k1); Node<K, V> result = null; boolean searched = false;//是否已经搜索过这个key do { parent = node; K k2 = node.key; int h2 = node.keyHash; // 增加规则,先比较hash值,提高效率 if (h1 > h2) { cmp = 1; } else if (h1 < h2) { cmp = -1; } else if (Objects.equals(k1, k2)) { cmp = 0; } else if (k1 != null && k2 != null && k1.getClass() == k2.getClass() && k1 instanceof Comparable && (cmp = ((Comparable) k1).compareTo(k2)) != 0) { // 再次增加一条规则,提高效率 } else if (searched) { // 已经扫描过了 cmp = System.identityHashCode(k1) - System.identityHashCode(k2); // 最后增加一条规则,方便调试,但是不会增加效率 } else { //未扫描,根据内存地址大小决定左右 if ((node.left != null && (result = node(node.left, k1)) != null) || (node.right != null && (result = node(node.right, k1)) != null)) { //已经存在这个key node = result; cmp = 0; } else { // 不存在这个key searched = true; cmp = System.identityHashCode(k1) - System.identityHashCode(k2); } } if (cmp > 0) { node = node.right; } else if (cmp < 0) { node = node.left; } else { V oldValue = node.value; node.key = key;//一般覆盖(不同对象可能有相同的比较参数) node.value = value; return oldValue; } } while (node != null); // 2.判断插入父节点的左子节点还是右子节点 Node<K, V> newNode = createNode(key, value, parent); if (cmp > 0) { parent.right = newNode; } else { parent.left = newNode; } size++; afterPut(newNode);//新添加节点之后的处理 return null; } /** * 桶数组扩容 */ private void resize() { if (size / table.length <= DEFAULT_LOAD_FACTOR) return; // 扩容 Node<K, V>[] oldTable = table; table = new Node[table.length << 1]; // 分析: // 当扩容为原来的两倍时,节点的索引有2种情况 // 1. 保持不变 // 2. index = index + 旧容量 // 层序遍历每个节点开始挪动 Queue<Node<K, V>> queue = new LinkedList<>(); for (Node<K, V> kvNode : oldTable) { if (kvNode == null) continue; queue.offer(kvNode); while (!queue.isEmpty()) { Node<K, V> node = queue.poll(); if (node.left != null) { queue.offer(node.left); } if (node.right != null) { queue.offer(node.right); } moveNode(node); } } } private void moveNode(Node<K, V> newNode) { // 重置node newNode.parent = null; newNode.left = null; newNode.right = null; newNode.color = RED; int index = index(newNode); // 取出index位置的红黑树根节点 Node<K, V> root = table[index]; if (root == null) { root = newNode; table[index] = root; afterPut(root); return; } // hash冲突,添加新节点到红黑树 // 1.找到待添加位置的父节点 Node<K, V> parent = root; Node<K, V> node = root; int cmp = 0; K k1 = newNode.key; int h1 = newNode.keyHash; do { parent = node; K k2 = node.key; int h2 = node.keyHash; // 增加规则,先比较hash值,提高效率 if (h1 > h2) { cmp = 1; } else if (h1 < h2) { cmp = -1; } else if (k1 != null && k2 != null && k1.getClass() == k2.getClass() && k1 instanceof Comparable && (cmp = ((Comparable) k1).compareTo(k2)) != 0) { // 再次增加一条规则,提高效率 } else { cmp = System.identityHashCode(k1) - System.identityHashCode(k2); } if (cmp > 0) { node = node.right; } else if (cmp < 0) { node = node.left; } } while (node != null); // 2.判断插入父节点的左子节点还是右子节点 newNode.parent = parent; if (cmp > 0) { parent.right = newNode; } else { parent.left = newNode; } afterPut(newNode);//新添加节点之后的处理 } private void afterPut(Node<K, V> node) { Node<K, V> parent = node.parent; //添加的是根节点 或 上溢到根节点 if (parent == null) { black(node); return; } //类型一:parent是黑色(不用处理四种情况) if (isBlack(parent)) return; //类型二:parent是红色且uncle是红色(会上溢的四种情况) Node<K, V> uncle = parent.getSibling(); Node<K, V> grand = red(parent.parent);//以下情况都需要将grand染成红色,可以统一处理 if (isRed(uncle)) { black(parent); black(uncle); //把祖父节点当作是新添加的节点 afterPut(grand);//上溢递归调用 return; } //类型三:parent是红色且uncle不是红色(需要旋转的四种情况) if (parent.isLeftChild()) {//L if (node.isLeftChild()) { //LL black(parent); } else { //LR black(node); rotateLeft(parent); } rotateRight(grand); } else { //R if (node.isLeftChild()) { //RL black(node); rotateRight(parent); } else { //RR black(parent); } rotateLeft(grand); } } /** * 给节点上色 */ private Node<K, V> color(Node<K, V> node, boolean color) { if (node == null) return node; node.color = color; return node; } /** * 将节点染成红色 */ private Node<K, V> red(Node<K, V> node) { return color(node, RED); } /** * 将节点染成黑色 */ private Node<K, V> black(Node<K, V> node) { return color(node, BLACK); } /** * 获取当前节点的颜色 */ private boolean colorOf(Node<K, V> node) { return node == null ? BLACK : node.color; } /** * 判断当前颜色是否为黑色 */ private boolean isBlack(Node<K, V> node) { return colorOf(node) == BLACK; } /** * 判断当前颜色是否为红色 */ private boolean isRed(Node<K, V> node) { return colorOf(node) == RED; } /** * 左旋转,以RR为例 */ private void rotateLeft(Node<K, V> grand) { Node<K, V> parent = grand.right; Node<K, V> child = parent.left;//child就是T1子树 grand.right = child; parent.left = grand; afterRotate(grand, parent, child); } /** * 右旋转,以LL为例 */ private void rotateRight(Node<K, V> grand) { Node<K, V> parent = grand.left; Node<K, V> child = parent.right; grand.left = child; parent.right = grand; afterRotate(grand, parent, child); } /** * 抽取左旋转和右旋转中的重复代码 */ private void afterRotate(Node<K, V> grand, Node<K, V> parent, Node<K, V> child) { //更新parent的parent(让parent成为子树的根节点) parent.parent = grand.parent; if (grand.isLeftChild()) { grand.parent.left = parent; } else if (grand.isRightChild()) { grand.parent.right = parent; } else { //grand是root节点 table[index(grand)] = parent; } //更新child的parent if (child != null) { child.parent = grand; } //更新grand的parent grand.parent = parent; } @Override public V get(K key) { Node<K, V> node = node(key); return node != null ? node.value : null; } private Node<K, V> node(K key) { Node<K, V> root = table[index(key)]; return root == null ? null : node(root, key); } private Node<K, V> node(Node<K, V> node, K k1) { int h1 = hash(k1); // 储存查找结果 Node<K, V> result = null; int cmp = 0; while (node != null) { K k2 = node.key; int h2 = node.keyHash; // 增加规则,先比较hash值,提高效率 if (h1 > h2) { node = node.right; } else if (h1 < h2) { node = node.left; } else if (Objects.equals(k1, k2)) { return node; } else if (k1 != null && k2 != null && k1.getClass() == k2.getClass() && k1 instanceof Comparable && (cmp = ((Comparable) k1).compareTo(k2)) != 0) { // 再次增加一条规则,提高效率 node = cmp > 0 ? node.right : node.left; } else if (node.right != null && (result = node(node.right, k1)) != null) { // 哈希值相等,不具备可比较性,也不equals return result; } else { //只能往左边扫 node = node.left; } } return null; } @Override public V remove(K key) { return remove(node(key)); } protected V remove(Node<K, V> node) { if (node == null) return null; Node<K, V> willNode = node; size--; V oldValue = node.value; //考虑度为2的节点,转化为度为1 if (node.hasTwoChildren()) { Node<K, V> s = successor(node);//后继节点 //用后继节点的值覆盖度为2的节点的值 node.key = s.key; node.value = s.value; node.keyHash = s.keyHash; //删除后继节点 node = s; } //删除node节点(能到这则说明node的度必为0或1) Node<K, V> replacement = node.left != null ? node.left : node.right; int index = index(node); if (replacement != null) { //node是度为1的节点 //更改parent replacement.parent = node.parent; //更改parent的left,right指向 if (node.parent == null) { //node是度为1的节点也是根节点 table[index] = replacement; } else if (node == node.parent.left) { node.parent.left = replacement; } else { //在右边 node.parent.right = replacement; } //此时开始恢复平衡(AVL树 或 RB树需要实现此方法) afterRemove(node, replacement); } else if (node.parent == null) { //node是叶子节点也是根节点 table[index] = null; afterRemove(node, null); } else { //node是叶子节点但不是根节点 if (node == node.parent.left) { node.parent.left = null; } else { node.parent.right = null; } //此时开始恢复平衡(AVL树 或RB树 需要实现此方法) afterRemove(node, null); } // 交给子类去处理 subclassAfterRemove(willNode, node); return oldValue; } protected void subclassAfterRemove(Node<K, V> willNode, Node<K, V> removedNode) {} /** * 实现删除节点后的处理操作 */ private void afterRemove(Node<K, V> node, Node<K, V> replacement) { //情况一:如果删除的节点是红色,不用处理 if (isRed(node)) return; //情况二:用于取代node子节点的是红色节点 if (isRed(replacement)) { black(replacement); return; } //情况三:删除的是黑色叶子节点(下溢) Node<K, V> parent = node.parent; //删除的是根节点 if (parent == null) return; //判断被删除的node的节点是左还是右 boolean left = parent.left == null || node.isLeftChild(); Node<K, V> sibling = left ? parent.right : parent.left; if (left) { //被删除的节点在左边,兄弟节点在右边(镜像对称处理) if (isRed(sibling)) { //兄弟节点是红色,就要转成黑色 black(sibling); red(parent); rotateLeft(parent); //更换兄弟 sibling = parent.right; } //兄弟节点必然是黑色 if (isBlack(sibling.left) && isBlack(sibling.right)) { //兄弟节点没有一个红色子节点,父节点要向下向子节点合并 boolean parentBlack = isBlack(parent); black(parent); red(sibling); if (parentBlack) { afterRemove(parent, null); } } else { //兄弟节点至少有 1 个红色节点,就要向兄弟节点借元素 if (isBlack(sibling.right)) { //兄弟节点的右边不是红色,则兄弟要先旋转 rotateRight(sibling); sibling = parent.right; } color(sibling, colorOf(parent)); black(sibling.right); black(parent); rotateLeft(parent); } } else { //被删除的节点在右边,兄弟节点在左边(图示的是这种) if (isRed(sibling)) { //兄弟节点是红色,就要转成黑色 black(sibling); red(parent); rotateRight(parent); //更换兄弟 sibling = parent.left; } //兄弟节点必然是黑色 if (isBlack(sibling.left) && isBlack(sibling.right)) { //兄弟节点没有一个红色子节点,父节点要向下向子节点合并 boolean parentBlack = isBlack(parent); black(parent); red(sibling); if (parentBlack) { afterRemove(parent, null); } } else { //兄弟节点至少有 1 个红色节点,就要向兄弟节点借元素 if (isBlack(sibling.left)) { //兄弟节点的左边不是红色,则兄弟要先旋转 rotateLeft(sibling); sibling = parent.left; } color(sibling, colorOf(parent)); black(sibling.left); black(parent); rotateRight(parent); } } } /** * 利用中序遍历求某个节点的后继节点 */ private Node<K, V> successor(Node<K, V> node) { if (node == null) return null; //前驱节点在右子树中:node.right.left.left... Node<K, V> p = node.right; if (p != null) { while (p.left != null) { p = p.left; } return p; } //从祖父节点中寻找前驱节点 while (node.parent != null && node == node.parent.right) { node = node.parent; } //情况一:node.parent == null ↓ //情况二:node == node.parent.left ↓ return node.parent; } @Override public boolean containsKey(K key) { return node(key) != null; } /** * 遍历每一个节点的value,红黑树使用层序遍历 * * @param value 每一个节点的value * @return bool */ @Override public boolean containsValue(V value) { if (size == 0) return false; Queue<Node<K, V>> queue = new LinkedList<>(); for (Node<K, V> kvNode : table) { if (kvNode == null) continue; queue.offer(kvNode); while (!queue.isEmpty()) { Node<K, V> node = queue.poll(); if (Objects.equals(value, node.value)) return true; if (node.left != null) { queue.offer(node.left); } if (node.right != null) { queue.offer(node.right); } } } return false; } @Override public void traversal(Visitor<K, V> visitor) { if (size == 0 || visitor == null) return; Queue<Node<K, V>> queue = new LinkedList<>(); for (Node<K, V> kvNode : table) { if (kvNode == null) continue; queue.offer(kvNode); while (!queue.isEmpty()) { Node<K, V> node = queue.poll(); if (visitor.visit(node.key, node.value)) return; if (node.left != null) { queue.offer(node.left); } if (node.right != null) { queue.offer(node.right); } } } } public void print() { if (size == 0) return; for (final Node<K, V> root : table) { BinaryTrees.println(new BinaryTreeInfo() { @Override public Object root() { return root; } @Override public Object left(Object node) { return ((Node<K, V>) node).left; } @Override public Object right(Object node) { return ((Node<K, V>) node).right; } @Override public Object string(Object node) { return node; } }); } } }

6. 哈希值的进一步处理:扰动计算

在上面的hashmap实现中,生成hash值时为什么还要再次高低16位做与运算?

==> 扰动计算,能使hash排布更加均匀!

private int hash(K key) { if (key == null) return 0; int h = key.hashCode(); return (h ^ (h >>> 16)) & (table.length - 1); }

7. 装填因子

在上面的hashmap实现中,在扩容时用到了 装填因子 !

装填因子(Load Factor):节点总数量 / 哈希表桶数组长度,也叫做负载因子

在JDK1.8的HashMap中,如果装填因子超过0.75,就扩容为原来的2倍

8. 关于使用%来计算索引

如果使用%来计算索引

  • 建议把哈希表的长度设计为素数(质数),可以大大减小哈希冲突
image-20220101165226216

下边表格列出了不同数据规模对应的最佳素数,特点如下

  • 每个素数略小于前一个素数的2倍
  • 每个素数尽可能接近2的幂(2^n)
image-20220101165328974

9. TreeMap VS HashMap

9.1 性能对比

image-20220103011201320

9.2 选择时机

何时选择 TreeMap? => 元素具备可比较性且要求升序遍历(按照元素从小到大)

何时选择 HashMap?=> 无序遍历

10.LinkedHashMap

10.1 理解

在HashMap的基础上维护元素的添加顺序,使得遍历的结果是遵从添加顺序的

假设添加顺序是:37、21、31、41、97、95、52、42、83

image-20220101164517665

10.2 LinkedHashMap实现

/** * @Description LinkedHashMap, 采取双向链表,提高效率 * @Author monap * @Date 2022/1/2 15:53 */ @SuppressWarnings("unchecked") public class LinkedHashMap<K, V> extends HashMap<K, V> { private LinkedHashNode<K, V> first; private LinkedHashNode<K, V> last; private static class LinkedHashNode<K, V> extends Node<K, V> { LinkedHashNode<K, V> prev; LinkedHashNode<K, V> next; public LinkedHashNode(K key, V value, Node<K, V> parent) { super(key, value, parent); } } @Override protected Node<K, V> createNode(K key, V value, Node<K, V> parent) { LinkedHashNode<K, V> node = new LinkedHashNode<>(key, value, parent); if (first == null) { first = last = node; } else { last.next = node; node.prev = last; last = node; } return node; } @Override public void clear() { super.clear(); first = null; last = null; } @Override protected void subclassAfterRemove(Node<K, V> willNode,Node<K, V> removedNode) { LinkedHashNode<K, V> node1 = (LinkedHashNode<K, V>) willNode; LinkedHashNode<K, V> node2 = (LinkedHashNode<K, V>) removedNode; if(node1 != node2) { // 交换linkedHashWillNode和linkedHashRemovedNode在链表中的位置 // 交换prev LinkedHashNode<K,V> tmp = node1.prev; node1.prev = node2.prev; node2.prev = tmp; if(node1.prev == null) { first = node1; } else { node1.prev.next = node1; } if(node2.prev == null) { first = node2; } else { node2.prev.next = node2; } //交换last tmp = node1.next; node1.next = node2.next; node2.next = tmp; if(node1.next == null) { last = node1; } else { node1.next.prev = node1; } if(node2.next == null) { last = node2; } else { node2.next.prev = node2; } } LinkedHashNode<K, V> prev = node2.prev; LinkedHashNode<K, V> next = node2.next; if (prev == null) { first = next; } else { prev.next = next; } if (next == null) { last = prev; } else { next.prev = prev; } } @Override public boolean containsValue(V value) { LinkedHashNode<K, V> node = first; while (node != null) { if (Objects.equals(value,node.value)) return true; node = node.next; } return false; } @Override public void traversal(Visitor<K, V> visitor) { if (visitor == null) return; LinkedHashNode<K, V> node = first; while (node != null) { if (visitor.visit(node.key, node.value)) return; node = node.next; } } }

注意:链表是跨红黑树的!

10.3 删除的注意点

删除度为2的节点node时(比如删除31),需要注意更换 node 与 前驱\后继节点 的连接位置

image-20220101164658085

10.4 更换节点的连接位置

image-20220101164731371

交换prev

LinkedNode<K,V> tmp = node1.prev; node1.prev = node2.prev; node2.prev = tmp; if (node1.prev != null) { node1.prev.next = node1; } else { first = node1; } if (node2.prev != null) { node2.prev.next = node2; } else { first = node2; }

交换next

tmp = node.next; node1.next = node2.next; node2.next = tmp; if (node1.next != null) { node1.next.prev = node1; } else { last = node1; } if (node2.next != null) { node2.next.prev = node2; } else { last = node2; }

11. HashSet

/** * @Description TODO * @Author monap * @Date 2022/1/3 2:25 */ public class HashSet<E> implements Set<E> { private HashMap<E,Object> map = new HashMap<>(); @Override public int size() { return map.size(); } @Override public boolean isEmpty() { return map.isEmpty(); } @Override public void clear() { map.clear(); } @Override public boolean contains(E element) { return map.containsKey(element); } @Override public void add(E element) { map.put(element,null); } @Override public void remove(E element) { map.remove(element); } @Override public void traversal(Visitor<E> visitor) { map.traversal(new Map.Visitor<E,Object>() { @Override public boolean visit(E key, Object value) { return visitor.visit(key); } }); } }

12. LinkedHashSet

/** * @Description TODO * @Author monap * @Date 2022/1/3 2:25 */ public class LinkedHashSet<E> implements Set<E> { private LinkedHashMap<E,Object> map = new LinkedHashMap<>(); @Override public int size() { return map.size(); } @Override public boolean isEmpty() { return map.isEmpty(); } @Override public void clear() { map.clear(); } @Override public boolean contains(E element) { return map.containsKey(element); } @Override public void add(E element) { map.put(element,null); } @Override public void remove(E element) { map.remove(element); } @Override public void traversal(Visitor<E> visitor) { map.traversal(new Map.Visitor<E,Object>() { @Override public boolean visit(E key, Object value) { return visitor.visit(key); } }); } }

九. 二叉堆

1. 引入

设计一种数据结构,用来存放整数,要求提供 3 个接口

  • 添加元素
  • 获取最大值
  • 删除最大值
image-20220103025909450

有没有更优的数据结构?=> 堆

  • 获取最大值:O(1)
  • 删除最大值:O(logn)
  • 添加元素:O(logn)

解决 Top K 问题

什么是 Top K 问题? => 从海量数据中找出前 K 个数据,比如从 100 万个整数中找出最大的 100 个整数

Top K 问题的解法之一:可以用数据结构“堆”来解决

2. 堆理解

堆(Heap)也是一种树状的数据结构(不要跟内存模型中的“堆空间”混淆),常见的堆实现有

  • 二叉堆(Binary Heap,完全二叉堆)
  • 多叉堆(D-heap、D-ary Heap)
  • 索引堆(Index Heap)
  • 二项堆(Binomial Heap)
  • 斐波那契堆(Fibonacci Heap)
  • 左倾堆(Leftist Heap,左式堆)
  • 斜堆(Skew Heap)

堆的一个重要性质:任意节点的值总是 ≥( ≤ )子节点的值

  • 如果任意节点的值总是 ≥ 子节点的值,称为:最大堆、大根堆、大顶堆

  • 如果任意节点的值总是 ≤ 子节点的值,称为:最小堆、小根堆、小顶堆

由此可见,堆中的元素必须具备可比较性(跟二叉搜索树一样)

3. 堆基本接口设计

image-20220103030637581

4. 二叉堆理解

二叉堆 的逻辑结构就是一棵完全二叉树,所以也叫 完全二叉堆

鉴于完全二叉树的一些特性,二叉堆的底层(物理结构)一般用数组实现即可

索引 i 的规律( n 是元素数量)

  • 如果 i = 0 ,它是 节点
  • 如果 i > 0 ,它的 节点的索引为 floor( (i – 1) / 2 )
  • 如果 2i + 1 ≤ n – 1,它的 子节点的索引为 2i + 1
  • 如果 2i + 1 > n – 1 ,它 无左子节点
  • 如果 2i + 2 ≤ n – 1 ,它的 子节点的索引为 2i + 2
  • 如果 2i + 2 > n – 1 ,它 无右子节点
image-20220103031011454

5. 最大堆-添加

image-20220105224635687

循环执行以下操作(图中的 80 简称为 node)

  • 如果 node > 父节点 ==> 与父节点交换位置
  • 如果 node ≤ 父节点,或者 node 没有父节点 ==> 退出循环

这个过程,叫做上滤(Sift Up),时间复杂度为 O(logn)

交换位置的优化

一般交换位置需要3行代码,可以进一步优化 ==> 将新添加节点备份,确定最终位置才摆放上去

仅从交换位置的代码角度看,可以由大概的 3 * O(logn) 优化到 1 * O(logn) + 1

image-20220105225124705

6. 最大堆-删除

image-20220105232454567
  • 用最后一个节点覆盖根节点

  • 删除最后一个节点

  • 循环执行以下操作(图中的 43 简称为 node)

    • 如果 node < 最大的子节点 ==> 与最大的子节点交换位置
    • 如果 node ≥ 最大的子节点, 或者 node 没有子节点 ==> 退出循环

这个过程,叫做下滤(Sift Down),时间复杂度:O(logn)

同样的,交换位置的操作可以像添加那样进行优化

7. 最大堆–批量建堆 (Heapify)

批量建堆,有 2 种做法

  • 自上而下的上滤
  • 自下而上的下滤

自上而下的上滤

image-20220105235809786

自下而上的下滤

image-20220105235841884

效率对比

image-20220105235916423

所有节点的深度之和

  • 仅仅是叶子节点,就有近 n/2 个,而且每一个叶子节点的深度都是 O(logn) 级别的
  • 因此,在叶子节点这一块,就达到了 O(nlogn) 级别
  • O(nlogn) 的时间复杂度足以利用排序算法对所有节点进行全排序

所有节点的高度之和

  • 假设是满树,节点总个数为 n,树高为 h,那么 n = 2^h − 1
  • 所有节点的树高之和
H(n) = 2^0 ∗ (h − 0) + 2^1 ∗ (h − 1) + 2^2 ∗ (h − 2) + ⋯ + 2^(h −1) ∗ [h − (h −1)] = h ∗ (2^0 + 2^1 + 2^2 + ⋯ + 2^(h −1) − [1 ∗ 2^1 + 2 ∗ 2^2 + 3 ∗ 2^3 + ⋯ + (h − 1) ∗ 2^(h−1) = h ∗ (2^h − 1) − [(h − 2) ∗ 2^h + 2] = h ∗ 2^h − h − h ∗ 2^h + 2 ^(h+1) − 2 = 2^(h+1) − h − 2 = 2 ∗ (2^h − 1) − h = 2n − h = 2n − log2(n + 1) = O(n)

公式推导

S(h) = 1 ∗ 2^1 + 2 ∗ 2^2 + 3 ∗ 2^3 + ⋯ + (h − 2) ∗ 2^(h−2) + (h − 1) ∗ 2^(h−1) 2S(h) = 1 ∗ 2^2 + 2 ∗ 2^3 + 3 ∗ 2^4 + ⋯ + (h − 2) ∗ 2^(h−1) + (h − 1) ∗ 2^h S(h) – 2S(h) = [2^1 + 2^2 + 2^3 + ⋯ + 2^(h−1)] − (h − 1) ∗ 2^h = (2^h − 2) − (h − 1) ∗ 2^h S(h) = (h − 1) ∗ 2^h − (2^h − 2) = (h − 2) ∗ 2^h + 2

疑惑

以下方法可以批量建堆么

  • 自上而下的下滤
  • 自下而上的上滤

上述方法不可行,为什么?

认真思考【自上而下的上滤】、【自下而上的下滤】的本质。自上而下的上滤的本质是添加,自下而上的下滤的本质是删除

8. 构建小顶堆

只需要改变一下比较策略即可,比如值比较小的节点更大

@Test public void testMinHeap() { Integer[] data = {88, 44, 53, 41, 16, 6, 70, 18, 85, 98, 81, 23}; BinaryHeap<Integer> heap = new BinaryHeap<>(data, new Comparator<Integer>() { @Override public int compare(Integer o1, Integer o2) { return o2 - o1; } }); BinaryTrees.println(heap); }

9. 大顶堆实现

抽象父类

/** * @Description 二叉堆 * @Author monap * @Date 2022/1/5 23:17 */ @SuppressWarnings("unchecked") public abstract class AbstractHeap<E> implements Heap<E> { protected int size; protected Comparator<E> comparator; public AbstractHeap(Comparator<E> comparator) { this.comparator = comparator; } public AbstractHeap() { this(null); } @Override public int size() { return size; } @Override public boolean isEmpty() { return size == 0; } protected int compare(E e1, E e2) { return comparator != null ? comparator.compare(e1, e2) : ((Comparable<E>) e1).compareTo(e2); } }

具体类

/** * @Description 二叉堆 * @Author monap * @Date 2022/1/5 22:30 */ @SuppressWarnings("unchecked") public class BinaryHeap<E> extends AbstractHeap<E> implements BinaryTreeInfo { private E[] elements; private static final int DEFAULT_CAPACITY = 10; public BinaryHeap(E[] elements, Comparator<E> comparator) { super(comparator); if (elements == null || elements.length == 0) { this.elements = (E[]) new Object[DEFAULT_CAPACITY]; } else { size = elements.length; int capacity = Math.max(elements.length, DEFAULT_CAPACITY); this.elements = (E[]) new Object[capacity]; System.arraycopy(elements, 0, this.elements, 0, capacity); heapify(); } } public BinaryHeap(E[] elements) { this(elements, null); } public BinaryHeap(Comparator<E> comparator) { this(null, comparator); } public BinaryHeap() { this(null, null); } /** * 批量建堆 */ private void heapify() { // 1.自上而下的上滤 // for (int i = 0; i < size; i++) { // siftUp(i); // } // 2. 自下而上的下滤 for (int i = (size >> 1) - 1; i >= 0; i--) { siftDown(i); } } @Override public void clear() { for (int i = 0; i < size; i++) { elements[i] = null; } } @Override public void add(E element) { elementNotNullCheck(element); ensureCapacity(size + 1); elements[size++] = element; siftUp(size - 1); } /** * 让index位置的元素上滤 * * @param index index */ private void siftUp(int index) { E e = elements[index]; while (index > 0) { int pIndex = (index - 1) >> 1; E p = elements[pIndex]; if (compare(e, p) <= 0) break; elements[index] = p; index = pIndex; } elements[index] = e; } @Override public E get() { emptyCheck(); return elements[0]; } /** * 删除堆顶元素 **/ @Override public E remove() { emptyCheck(); int lastIndex = --size; E root = elements[0]; elements[0] = elements[lastIndex]; elements[lastIndex] = null; siftDown(0); return root; } private void siftDown(int index) { E element = elements[index]; // 第一个叶子节点的索引即为非叶子节点的数量 int half = size >> 1; // 必须保证index位置必须为非叶子节点 while (index < half) { //index的节点有两种情况 // 1.只有左子节点 // 2.同时拥有左右节点 // 默认为左子节点的索引跟它比较 int childIndex = (index << 1) + 1; E child = elements[childIndex]; // 右子节点 int rightIndex = childIndex + 1; // 选出左右子节点中最大的那个 if (rightIndex < size && compare(elements[rightIndex], child) > 0) { child = elements[childIndex = rightIndex]; } if (compare(element, child) >= 0) break; //将子节点存放到index位置 elements[index] = child; //重新设置index index = childIndex; } elements[index] = element; } /** * 删除堆顶元素的同时插入一个新元素 * * @param element element * @return E */ @Override public E replace(E element) { elementNotNullCheck(element); E root = null; if (size == 0) { elements[0] = element; size++; } else { root = elements[0]; elements[0] = element; siftDown(0); } return root; } private void emptyCheck() { if (size == 0) { throw new IndexOutOfBoundsException("Heap is empty"); } } private void ensureCapacity(int capacity) { int oldCapacity = elements.length; if (capacity < oldCapacity) return; int newCapacity = oldCapacity + (oldCapacity >> 1);//1.5倍 E[] newElements = (E[]) new Object[newCapacity]; System.arraycopy(elements, 0, newElements, 0, elements.length); elements = newElements; System.out.println("扩容:" + oldCapacity + "=>" + newCapacity); } private void elementNotNullCheck(E element) { if (element == null) { throw new IllegalArgumentException("element mush not be empty"); } } @Override public Object root() { return 0; } @Override public Object left(Object node) { int index = ((int) node << 1) + 1; return index >= size ? null : index; } @Override public Object right(Object node) { int index = ((int) node << 1) + 2; return index >= size ? null : index; } @Override public Object string(Object node) { return elements[(int) node]; } }

10. Top K 问题

从 n 个整数中,找出最大的前 k 个数( k 远远小于 n )

如果使用排序算法进行全排序,需要 O(nlogn) 的时间复杂度

如果使用二叉堆来解决,可以使用 O(nlogk) 的时间复杂度来解决

  • 新建一个小顶堆
  • 扫描 n 个整数
    • 先将遍历到的前 k 个数放入堆中
    • 从第 k + 1 个数开始,如果大于堆顶元素,就使用 replace 操作(删除堆顶元素,将第 k + 1 个数添加到堆中)
  • 扫描完毕后,堆中剩下的就是最大的前 k 个数

如果是找出最小的前 k 个数呢?

  • 用大顶堆
  • 如果小于堆顶元素,就使用 replace 操作
/** * 找出下面数组中最大的5个数 */ @Test public void testTopK() { int k = 5; Integer[] data = {51, 30, 39, 92, 74, 25, 16, 93, 91, 19, 54, 47, 73, 62, 76, 63, 35, 18, 90, 6, 65, 49, 3, 26, 61, 48}; BinaryHeap<Integer> heap = new BinaryHeap<>((o1, o2) -> o2 - o1); for (Integer datum : data) { if (heap.size() < k) { heap.add(datum); } else if (datum > heap.get()) { heap.replace(datum); } } BinaryTrees.println(heap); } // ┌─76─┐ // │ │ // ┌─90─┐ 93 // │ │ // 92 91

十. 优先级队列

1. 接口设计

优先级队列也是个队列,因此也是提供以下接口

image-20220106012111683

普通的队列是 FIFO 原则,也就是先进先出

优先级队列则是按照优先级高低进行出队,比如将优先级最高的元素作为队头优先出队

2. 应用场景

医院的夜间门诊

  • 队列元素是病人
  • 优先级是病情的严重情况、挂号时间

操作系统的多任务调度

  • 队列元素是任务

  • 优先级是任务类型

3. 实现

根据优先队列的特点,很容易想到:可以直接利用二叉堆作为优先队列的底层实现

可以通过 Comparator 或 Comparable 去自定义优先级高低

/** * @Description 优先级队列 * @Author monap * @Date 2022/1/6 1:26 */ public class PriorityQueue<E> { private BinaryHeap<E> heap; public PriorityQueue(Comparator<E> comparator) { heap = new BinaryHeap<>(comparator); } public PriorityQueue() { this(null); } //元素的数量 public int size() { return heap.size(); } // 判断队列是否为空 public boolean isEmpty() { return heap.isEmpty(); } // 入队 public void enQueue(E element) { heap.add(element); } // 出队 public E deQueue() { return heap.remove(); } // 清空队列元素 public void clear() { heap.clear(); } // 获取优先级最高的元素 public E front() { return heap.get(); } }

十一. 哈夫曼树

1. 哈夫曼编码

哈夫曼编码,又称为霍夫曼编码(Huffman Coding),它是现代压缩算法的基础

假设要把字符串【ABBBCCCCCCCCDDDDDDEE】转成二进制编码进行传输

  • 可以转成ASCII编码(6569,10000011000101),但是有点冗长,如果希望编码更短呢?

  • 可以先约定5个字母对应的二进制。对应的二进制编码:000001001001010010010010010010010010011011011011011011100100 ,一共20个字母,转成了60个二进制位

image-20220106014306287
  • 如果使用哈夫曼编码,可以压缩至41个二进制位,约为原来长度的68.3%

2. 哈夫曼树

先计算出每个字母的出现频率(权值,这里直接用出现次数),【ABBBCCCCCCCCDDDDDDEE】

image-20220106014435079

利用这些权值,构建一棵哈夫曼树(又称为霍夫曼树、最优二叉树)

如何构建一棵哈夫曼树?(假设有 n 个权值)

  • 以权值作为根节点构建 n 棵二叉树,组成森林
  • 在森林中选出 2 个根节点最小的树合并,作为一棵新树的左右子树,且新树的根节点为其左右子树根节点之和
  • 从森林中删除刚才选取的 2 棵树,并将新树加入森
  • 重复 2、3 步骤,直到森林只剩一棵树为止,该树即为哈夫曼树

3. 构建哈夫曼树

image-20220106014544666

4. 构建哈夫曼编码

image-20220106014637345

left为0,right为1,可以得出5个字母对应的哈夫曼编码

image-20220106014709221

【ABBBCCCCCCCCDDDDDDEE】的哈夫曼编码是 1110110110110000000001010101010101111

总结

  • n 个权值构建出来的哈夫曼树拥有 n 个叶子节点
  • 每个哈夫曼编码都不是另一个哈夫曼编码的前缀
  • 哈夫曼树是带权路径长度最短的树,权值较大的节点离根节点较近
  • 带权路径长度:树中所有的叶子节点的权值乘上其到根节点的路径长度。与最终的哈夫曼编码总长度成正比关系。

十二 Trie 字典树

1. 引入

需求

如何判断一堆不重复的字符串是否以某个前缀开头?我们可以用Set或Map存储字符串,遍历所有字符串进行判断。时间复杂度为O(n)

有没有更优的数据结构实现前缀搜索?有!那就是Trie

Trie理解

Trie 也叫做字典树、前缀树(Prefix Tree)、单词查找树

Trie 搜索字符串的效率主要跟字符串的长度有关

假设使用 Trie 存储 cat、dog、doggy、does、cast、add 六个单词

image-20220106014851941

2. 接口设计

有两种接口形式,可以分别用Set和Map实现。Map可以做到在存储字符串的同时储存其对应的value(如人的姓名和其对应的电话号码)

image-20220106212223906

3. 实现

/** * @Description 字典树 * @Author monap * @Date 2022/1/6 21:19 */ public class Trie<V> { private int size; private Node<V> root; private static class Node<V> { Node<V> parent; HashMap<Character, Node<V>> children; Character character; V value; boolean word; // 是否为单词 public Node(Node<V> parent) { this.parent = parent; } } public int size() { return size; } public boolean isEmpty() { return size == 0; } public void clear() { size = 0; root = null; } public V get(String key) { Node<V> node = node(key); return node != null && node.word ? node.value : null; } public boolean contains(String key) { Node<V> node = node(key); return node != null && node.word; } public V add(String key, V value) { keyCheck(key); if (root == null) { root = new Node<>(null); } Node<V> node = root; int len = key.length(); for (int i = 0; i < len; i++) { char c = key.charAt(i); boolean emptyChildren = node.children == null; Node<V> childNode = emptyChildren ? null : node.children.get(c); if (childNode == null) { childNode = new Node<>(node); childNode.character = c; node.children = emptyChildren ? new HashMap<>() : node.children; node.children.put(c, childNode); } node = childNode; } if (node.word) { V oldValue = node.value; node.value = value; return oldValue; } node.word = true; node.value = value; size++; return null; } public V remove(String key) { // 找到最后一个节点 Node<V> node = node(key); // 如果不是单词结尾,不做任何处理 if (node == null || !node.word) return null; size--; V oldValue = node.value; // 如果还有子节点 if (node.children != null && !node.children.isEmpty()) { node.word = false; node.value = null; return oldValue; } // 如果没有子节点 Node<V> parent; while ((parent = node.parent) != null) { parent.children.remove(node.character); if (parent.word || !parent.children.isEmpty()) break; node = parent; } return oldValue; } public boolean startWith(String prefix) { return node(prefix) != null; } private Node<V> node(String key) { keyCheck(key); Node<V> node = root; int len = key.length(); for (int i = 0; i < len; i++) { if (node == null || node.children == null || node.children.isEmpty()) return null; char c = key.charAt(i); node = node.children.get(c); } return node; } private void keyCheck(String key) { if (key == null || key.length() == 0) { throw new IllegalArgumentException("key must not empty"); } } }

4. 总结

Trie 的优点:搜索前缀的效率主要跟前缀的长度有关

Trie 的缺点:需要耗费大量的内存,因此还有待改进

更多Trie 相关的数据结构和算法

  • Double-array Trie
  • Suffix Tree
  • Patricia Tree
  • Crit-bit Tree
  • AC自动机

__EOF__

本文作者MPolaris
本文链接https://www.cnblogs.com/mpolaris/p/15773493.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。您的鼓励是博主的最大动力!
posted @   MPolaris  阅读(126)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
点击右上角即可分享
微信分享提示