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...
-
它后一个数等于前面两个数的和
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)
说明:
① 常见的算法时间复杂度由小到大依次为:Ο(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 理解
数组是一种顺序存储的线性表,所有元素的内存地址是连续的。
在很多编程语言中,数组都有个致命的缺点:无法动态修改容量。实际开发中,我们更希望数组的容量是可以动态改变的。
1.1 属性设计
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)
添加元素-add(int index,E element)
删除元素-remove(int index)
如何扩容
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 图解方法
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)只用修改添加和删除。
/**
* @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的那个人又出列,依此类推,直到所有人出列为止,由此产生一个出列编号的序列。
注意:约瑟夫问题也可以用其它数据结构解决,不一定要用循环链表,但是循环链表解决此问题很简单。
使用循环链表解决约瑟夫问题
为了发挥循环链表的最大威力,可对CircleLinkedList做如下改进:
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)的概念如下:
注意:这里说的“栈”与内存中的“栈空间”是两个不同的概念。
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 栈的应用-综合计算器(自定义优先级)
即使用栈计算一个中缀表达式的结果
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
中缀表达式的求值是我们人最熟悉的,但是对计算机来说却不好操作(上述案例就能看的这个问题),因此,在计算结果时,往往会将中缀表达式转成其它表达式来操作(一般转成后缀表达式)
后缀表达式
与前缀表达式相似,只是运算符位于操作数之后
后缀表达式的计算机求值
从左至右
扫描表达式,遇到数字时,将数字压入堆栈,遇到运算符时,弹出栈顶的两个数,用运算符对它们做相应的计算(次顶元素 和 栈顶元素),并将结果入栈;重复上述过程直到表达式最右端,最后运算得出的值即为表达式的结果
//例如: (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中的元素并输出,结果的逆序即为中缀表达式对应的后缀表达式
使用栈实现逆波兰计算器(计算整数)
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)
。
注意:队列优先使用双向链表实现,因为队列主要是往头尾操作元素。
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
双端队列是能在头尾两端添加、删除的队列。
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. 理解
递归就是 方法自己调用自己,每次调用时传入不同的变量
。递归有助于编程者解决复杂的问题,同时可以让代码变得简洁。
2. 应用场景
- 各种数学问题如: 8皇后问题 , 汉诺塔, 阶乘问题, 迷宫问题, 球和篮子的问题(google编程大赛)
- 各种算法中也会使用到递归,比如快排,归并排序,二分查找,分治算法等
- 用栈解决的问题 => 使用递归可以让代码更简洁
3. 递归需要遵守的重要规则
- 执行一个方法时,就创建一个
新的受保护的独立空间
(栈空间)。 - 方法的
局部变量是独立
的,不会相互影响, 比如上述n变量。 - 如果方法中使用的是引用类型变量(比如数组),就会
共享该引用类型的数据
。 - 递归
必须向退出递归的条件逼近
,否则就是无限递归,出现StackOverflowError栈溢出了。 - 当一个方法执行完毕,或者遇到return,就会返回,遵守
谁调用,就将结果返回给谁
,同时当方法执行完毕或者返回时,该方法也就执行完毕。
4. 递归的应用-迷宫问题
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. 递归的应用-八皇后问题(回溯算法)
思路分析
-
第一个皇后先放第一行第一列。
-
第二个皇后放在第二行第一列、然后判断是否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。
2.4 满二叉树
理解:最后一层节点的度都为 0,其他节点的度都为 2。
注意:
① 在同样高度的二叉树中,满二叉树的叶子节点数量最多、总节点数量最多。
② 满二叉树一定是真二叉树,真二叉树不一定是满二叉树。
2.5 完全二叉树
理解:对节点从上至下、左至右开始编号,其所有编号都能与相同高度的满二叉树中的编号对应。
注意:
① 叶子节点只会出现最后 2 层,最后 1 层的叶子结点都靠左对齐。
② 完全二叉树从根结点至倒数第 2 层是一棵满二叉树。
③ 满二叉树一定是完全二叉树,完全二叉树不一定是满二叉树。
性质:
// 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除法默认向下取整
判断一棵树是否为完全二叉树:
- 思路一:
- 思路二:
2.6 二叉树的遍历
遍历是数据结构中的常见操作:把所有元素都访问一遍
线性数据结构的遍历比较简单
- 正序遍历
- 逆序遍历
根据节点访问顺序的不同,二叉树的常见遍历方式有4种
- 前序遍历(Preorder Traversal)
- 中序遍历(Inorder Traversal)
- 后序遍历(Postorder Traversal)
- 层序遍历(Level Order Traversal)
前序遍历(递归/迭代)
访问顺序 :根节点
、前序遍历左子树、前序遍历右子树
应用:树状结构展示(注意左右子树的顺序)
中序遍历(递归/迭代)
访问顺序 :中序遍历左子树、根节点
、中序遍历右子树
应用:二叉搜索树的中序遍历按升序或降序处理节点
后序遍历(递归 / 迭代)
访问顺序 :后序遍历左子树、后序遍历右子树、根节点
应用:适用于一些先子后父的操作
层序遍历 (迭代实现:队列)
//实现思路
// 1. 将根节点入队
// 2. 循环执行以下操作,直到队列为空
// ① 将队头节点 A 出队,进行访问
// ② 将 A 的左子节点入队
// ③ 将 A 的右子节点入队
访问顺序 :从上到下、从左到右依次访问每一个节点
应用:① 计算二叉树的高度 ② 判断一棵树是否为完全二叉树
2.7 二叉树遍历的应用
- 前序遍历:树状结构展示(注意左右子树的顺序)
- 中序遍历:二叉搜索树的中序遍历按升序或者降序处理节点
- 后序遍历:适用于一些先子后父的操作
- 层序遍历:①计算二叉树的高度。②判断一棵树是否为完全二叉树
2.8 根据遍历结果重构二叉树
- 前序遍历 + 中序遍历 => 唯一的一颗二叉树
- 后序遍历 + 中序遍历 => 唯一的一颗二叉树
- 前序遍历 + 后序遍历 => 如果它是一棵真二叉树,结果是唯一的 。否则不然结果不唯一 。
2.9 前驱节点(predecessor)
理解:中序遍历时的前一个节点 “删除节点要使用该知识”
如果是二叉搜索树,前驱节点就是前一个比它小的节点
2.9 后继节点(successor)
理解:中序遍历时的后一个节点 “删除节点要使用该知识”
如果是二叉搜索树,后继节点就是后一个比它大的节点
2.10 打印二叉树的工具
https://github.com/CoderMJLee/BinaryTrees
使用步骤:
- 实现 BinaryTreeInfo 接口
- 调用打印API :BinaryTrees.println(bst);
2.11 二叉树代码实现
/**
* @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) {
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) {
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) {
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有关
3.2 理解
二叉搜索树是二叉树的一种,是应用非常广泛的一种二叉树,英文简称为 BST。
-
又被称为:二叉查找树、二叉排序树
-
任意一个节点的值都大于其左子树所有节点的值
-
任意一个节点的值都小于其右子树所有节点的值
-
它的左右子树也是一棵二叉搜索树
二叉搜索树可以大大提高搜索数据的效率
二叉搜索树存储的元素必须具备可比较性
- 比如 int、double 等
- 如果是自定义类型,需要指定比较方式
- 不允许为 null
3.3 接口设计
由于二叉搜索树继承于二叉树,只需要实现添加,删除,包含的接口
注意:对于当前使用的二叉树来说,它的元素没有索引的概念。
3.4 图解
添加节点
删除节点
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)级别 ”
注意:
① AVL树是最早发明的自平衡二叉搜索树之一
② AVL 取名于两位发明者的名字 :G. M. Adelson-Velsky 和 E. M. Landis(来自苏联的科学家)
4.2 继承机构
4.3 添加导致的失衡
示例:往下面这棵子树中添加 13
-
最坏情况:可能会导致所有祖先节点都失衡
-
父节点、非祖先节点,都不可能失衡
四种添加失衡情况及其处理(有且仅有四种)
- LL-g右旋转(单旋)
- RR-g左旋转(单旋)
- LR-p左旋转,g右旋转(双旋)
- RL-p右旋转,g左旋转(双旋)
四种添加失衡情况的统一处理
4.4 删除导致的失衡
示例:删除下面这棵树的16
删除后需要使用 LL-右旋转 解决失衡的情况
- 如果绿色节点不存在,更高层的祖先节点可能也会失衡,需要再次恢复平衡,然后又可能导致更高层的祖先节点失衡...
- 极端情况下,所有祖先节点都需要进行恢复平衡的操作,共 O(logn) 次调整
删除后需要使用 RR-左旋转 解决失衡的情况
删除后需要使用 LR-p左旋转,g右旋转(双旋) 解决失衡的情况
删除后需要使用 RL-p右旋转,g左旋转(双旋) 解决失衡的情况
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树 是一种 平衡的多路搜索树
,多用于文件系统、数据库的实现
仔细观察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 代合并
5.4 B树搜索
跟二叉搜索树的搜索类似
-
先在节点内部从小到大开始搜索元素
-
如果命中,搜索结束
-
如果未命中,再去对应的子节点中搜索元素,重复步骤 1
5.5 B树添加
新添加的元素必定是添加到 叶子节点 中 √ 红黑树会用到这个结论
- 插入55
- 插入98
-
再插入 98 呢?(假设这是一棵 4阶B树)
- 最右下角的叶子节点的元素个数将超过限制
- 这种现象可以称之为:上溢(overflow)
添加 – 上溢的解决(假设5阶)
-
上溢节点的元素个数必然等于 m
-
假设上溢节点最中间元素的位置为 k
-
将 k 位置的元素向上与父节点合并
-
将 [0, k-1] 和 [k + 1, m - 1] 位置的元素分裂成 2 个子节点
- 这 2 个子节点的元素个数,必然都不会低于最低限制(┌ m/2 ┐ − 1)
-
-
一次分裂完毕后,有可能导致父节点上溢,依然按照上述方法解决
- 最极端的情况,有可能一直分裂到根节点。如果一直传播到根节点就会导致B树变高(仅此一种情况导致B树变高)
插入98
插入52
插入54
5.6 B树删除
如果需要删除的元素在 叶子节点 中,那么直接删除即可
如果需要删除的元素在 非叶子节点 中
- 先找到前驱或后继元素,覆盖所需删除元素的值
- 再把前驱或后继元素删除
-
非叶子节点
的前驱或后继元素,必定在叶子节点
中- 所以这里的删除前驱或后继元素 ,就是最开始提到的情况:删除的元素在叶子节点中
-
真正的删除元素都是发生在叶子节点中
√红黑树会用到这个结论
删除-非叶子节点的 下溢 现象
-
删除 22 ?(假设这是一棵 5阶B树)
- 叶子节点被删掉一个元素后,元素个数可能会低于最低限制( 即
┌ m/2 ┐−1
) - 这种现象称为:
**下溢(underflow)**
- 叶子节点被删掉一个元素后,元素个数可能会低于最低限制( 即
删除-非叶子节点的 下溢 解决
-
下溢节点的元素数量必然等于
**┌ m/2 ┐ − 2**
-
如果下溢节点临近的兄弟节点,有至少
**┌ m/2 ┐**
个元素,可以向其借一个元素- 将父节点的元素
**b**
插入到下溢节点的**0**
位置(最小位置) - 用兄弟节点的元素
**a**
(最大的元素)替代父节点的元素 b - 这种操作其实就是:
**旋转**
- 将父节点的元素
注意:因为 b > a,所以不能破环二叉搜索树的性质直接将a放到下溢节点去。
- 如果下溢节点临近的兄弟节点,只有
**┌ m/2 ┐ − 1**
个元素 - 将父节点的元素 b 挪下来跟左右子节点进行合并
- 合并后的节点元素个数等于
**┌ m/2 ┐ + ┌ m/2 ┐ − 2**
,不会超过**m − 1**
上溢 - 这个操作可能会导致父节点下溢,依然按照上述方法解决,下溢现象可能会一直往上传播。
如果一直传播到根节点就会导致B树变矮(仅此一种情况导致B树变矮)
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
节点
注意:红黑树的
叶子节点
是让原来度为 0 的节点或度为 1 的节点都变成度为 2 的节点后的叶子节点。(增加空节点 null 实现此功能)此时红黑树就变成了真二叉树。
注意:之后展示的红黑树都会省略 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树 并不能完美匹配 红 黑树 的所有情况
6.3 红黑树 与 2-3-4树 的比较
如果下图最底层的 BLACK 节点是不存在的,在B树中是什么样的情形?=>整棵B树只有1个节点,而且是超级节点
6.4 添加节点
已知:
-
B树中,新元素必定是添加到叶子节点中
-
4阶B树所有节点的元素个数 x 都符合 1 ≤ x ≤ 3
注意:
① 建议新添加的节点默认为
RED
,这样能够让红黑树的性质尽快满足(性质1,2,3,5 都满足,性质 4 不一定)② 如果添加的是根节点,染成
BLACK
即可
添加的所有情况
有 4 种情况既满足红黑树的性质四:parent 为 BLACK
,同时也满足4阶B树的性质,因此不用做任何额外的处理。
有 8 种情况不满足红黑树的性质四:parent 为 RED
( Double Red ),其中前 4 种属于B树节点上溢的情况
添加 – 修复性质4 – LL\RR
判定条件:uncle 不是 RED
-
parent 染成
BLACK
,grand 染成RED
-
grand 进行单旋操作:
- LL:右旋转
- RR:左旋转
添加 – 修复性质4 – LR\RL
判定条件:uncle 不是 RED
-
自己染成
BLACK
,grand 染成RED
-
进行双旋操作:
- LR:parent 左旋转, grand 右旋转
- RL:parent 右旋转, grand 左旋转
添加 – 修复性质4 – 上溢 – LL
注意:之前修复的四种情况,添加节点的叔父节点都为null(null默认记为黑色)。
判定条件:uncle 是 RED
- parent、uncle 染成
BLACK
- grand 向上合并,且染成
RED
,当做是新添加的节点进行处理
grand 向上合并时,可能继续发生上溢
若上溢持续到根节点,只需将根节点染成 BLACK
添加 – 修复性质4 – 上溢 – RR
判定条件:uncle 是 RED
- parent、uncle 染成
BLACK
- grand 向上合并,且染成
RED
,当做是新添加的节点进行处理
添加 – 修复性质4 – 上溢 – LR
判定条件:uncle 是 RED
- parent、uncle 染成
BLACK
- grand 向上合并,且染成
RED
,当做是新添加的节点进行处理
添加 – 修复性质4 – 上溢 – RL
判定条件:uncle 是 RED
- parent、uncle 染成
BLACK
- grand 向上合并,且染成
RED
,当做是新添加的节点进行处理
6.4 删除节点
已知:B树中,最后真正被删除的元素都在叶子节点中
删除-RED节点
直接删除,不用做任何调整
删除 – 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
即可保持红黑树性质
删除 - BLACK 叶子节点 - sibling为 BLACK
BLACK
叶子节点被删除后,会导致B树节点下溢(比如删除88)
判定条件:如果 sibling 至少有 1 个 RED
子节点
- 进行旋转操作
- 旋转之后的中心节点继承 parent 的颜色
- 旋转之后的左右节点染为
BLACK
判定条件:如果 sibling 没有 RED
子节点
- 将 sibling 染成
RED
、parent 染成BLACK
即可修复红黑树性质 (合并) - 如果 parent 是
BLACK
会导致 parent 也下溢,这时只需要把 parent 当做被删除的节点处理即可(递归)
删除 - BLACK 叶子节点 - sibling为 RED
如果 sibling 是 RED
- sibling 染成
BLACK
,parent 染成RED
,进行旋转 - 于是又回到 sibling 是
*BLACK
的情况
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 != List.ELEMENT_NOT_FOUND) {
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 等)
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 contaionsKey(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 contaionsKey(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
注意:在实际应用中很多时候的需求:Map 中存储的元素不需要讲究顺序,Map 中的 Key 不需要具备可比较性。其实不考虑顺序、不考虑 Key 的可比较性,Map 有更好的实现方案,平均时间复杂度可以达到 O(1) ,那就是采取
哈希表来实现 Map
2. 哈希冲突(Hash Collision)
哈希冲突也叫做 哈希碰撞
- 2 个不同的 key,经过哈希函数计算出相同的结果
- key1 ≠ key2 ,hash(key1) = hash(key2)
解决哈希冲突的常见方法
-
开放定址法(Open Addressing)
即按照一定规则向其他地址探测,直到遇到空桶 。 -
再哈希法(Re-Hashing)
即设计多个哈希函数 -
链地址法(Separate Chaining)
即比如通过链表将同一index的元素串起来
JDK1.8的哈希冲突解决方案
- 默认使用
单向链表
将元素串起来(链地址法
) - 在添加元素时,可能会由
单向链表
转为红黑树
来存储元素。比如当哈希表容量 ≥ 64 且 单向链表的节点数量大于 8 时 - 当
红黑树
节点数量少到一定程度时,又会转为单向链表
- JDK1.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);
}
// 10000 = 2^4
// 01111 = 2^4 - 1 = 1111
// 假设哈希值为1001010 =>
// 1001010
// & 0001111
// ----------
// 0001010 => 生成的值的范围是 0000 ~ 1111
良好的哈希函数 能让哈希值更加均匀分布 → 减少哈希冲突次数 → 提升哈希表的性能
4. 如何生成key的哈希值
key 的常见种类可能有
-
整数、浮点数、字符串、自定义对象
-
不同种类的 key,哈希值的生成方式不一样,但目标是一致的
-
尽量让每个 key 的哈希值是唯一的
-
尽量让 key 的所有信息参与运算
-
在Java中,HashMap 的 key 必须实现 hashCode
、equals
方法,也允许 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了。
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());
}
自定义对象的哈希值
注意:
① 哈希值太大,整型溢出怎么办? => 不用作任何处理,溢出了还是一个整 数。
② 不重写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);
}
}