思考:
1、什么是队列?有哪些实现方式?
先入先出,添尾删头。可以通过链表、环形数组实现。
2、什么是泛型?什么时候用泛型?为什么要用它?有什么好处?
泛型是JDK5后引入的特性,本质是参数化类型。它提供了编译时类型安全检测机制,只支持引用数据类型。
当处于一下两种场景的时候就可以用:
- 定义类、方法、接口的时候,如果类型不确定,就可以定义泛型
- 如果类型不确定,但能知道是哪个继承体系中的,可以使用泛型的通配符。
优点:
- 类型安全,编译时检查和约束类型。
- 提高代码可读性和可维护性
- 避免类型转换和重复代码
- 允许设计者在实现时限制类型
如果想深入了解泛型请访问此文章—(2条消息) 面试题——深入理解Java泛型机制_如果我是枫的博客-CSDN博客
3、用单向环形带哨兵链表如何实现?
-
首先定义一个简化的队列接口
-
然后定义泛型结点类,定义属性head、tail、size(节点数)、capacity(队列容量),写构造方法,tail.next = head形成循环。
-
继承定义好的接口重写方法
- 向队列尾部插入一个元素。如果队列已满(达到容量上限), 则返回 false,否则创建一个新的节点并插入到尾部,并更新 tail 的引用, 同时增加 size 的计数。返回 true 表示插入成功。
- 从队列头部移除并返回一个元素。如果队列为空,返回 null。将头节点的下一个节点作为新的头节点,并更新 tail 的引用。 同时减少 size 的计数。
- 返回队列头部的元素,但不移除它。如果队列为空,返回 null。
- 判断队列是否为空,即判断头节点和尾节点是否相同。
- 判断队列是否已满,即判断队列的大小是否达到容量上限。
- 实现了 Iterable 接口,返回一个迭代器,用于遍历队列中的元素。迭代器通过 p 指针依次访问每个节点,并返回节点的值。
4、环形数组如何实现?有哪些问题?怎么优化?
环形数组实现有三种判空、满方式:
-
仅用 head, tail 判断空满, head, tail 即为索引值
- 在构造方法中,通过创建一个容量为 capacity + 1 的数组来存储元素,空余一个位置来区分队列的空和满。
- 当head == tail时,队列为空
- (tail + 1) % array.length == head判满
- 优点是只需要使用头尾指针就可以判空判满,缺点是要留一个位置存尾指针
-
用size 辅助判断空满
-
head和tail不让他们存储计算结果,单纯作为不断递增的指针值(整数),用到索引时,再用它们进行计算。但这样head 和 tail会不断递增,会出现以下一些问题:
- 如何保证 head 和 tail 自增超过正整数最大值的正确性?
- 如何让取模运算性能更高?
解决思路:
我们首先使用Integer.toUnsignedLong(),将负数转化为更大类型Long,让它不超。但这种方式太蠢了,接着我们使用无符右移求模运算,又发现余数正好为2^n-1 按位与运算性能更好,而且只找后几位,不用考虑符号位的问题,最后为了解决传入值的不是2的n次方的情况,我们采用了抛异常或将不是2^n 改成2^n 的解决方案。
5、二叉树层序遍历有哪些遍历方法?
二叉树层序遍历有两种思路:
- BFS 方式使用辅助队列进行广度优先搜索,每次处理一层。
- DFS 方式使用递归进行深度优先搜索,将节点值按层级添加到结果列表中。
一、 队列
1、概述
queue 是以顺序的方式维护的一组数据集合,在一端添加数据,从另一端移除数据。习惯来说,添加的一端称为尾,移除的一端称为头,就如同生活中的排队买商品
先定义一个简化的队列接口
public interface Queue<E> {
/**
* 向队列尾插入值
* @param value 待插入值
* @return 插入成功返回 true, 插入失败返回 false
*/
boolean offer(E value);
/**
* 从对列头获取值, 并移除
* @return 如果队列非空返回对头值, 否则返回 null
*/
E poll();
/**
* 从对列头获取值, 不移除
* @return 如果队列非空返回对头值, 否则返回 null
*/
E peek();
/**
* 检查队列是否为空
* @return 空返回 true, 否则返回 false
*/
boolean isEmpty();
/**
* 检查队列是否已满
* @return 满返回 true, 否则返回 false
*/
boolean isFull();
}
2、链表实现
下面以单向环形带哨兵链表方式来实现队列
代码
/**
* 基于单向环形链表实现
* @param <E> 队列中元素类型
*/
public class LinkedListQueue <E> implements Queue<E>,Iterable<E>{
//代码中使用了一个内部类 Node<E>,表示队列中的节点,每个节点包含一个值 value 和指向下一个节点的引用 next。
private static class Node<E>{
E value;
Node<E> next;
public Node(E value, Node<E> next) {
this.value = value;
this.next = next;
}
}
private Node<E> head = new Node<>(null,null);
private Node<E> tail = head;
//节点数
private int size;
//队列容量
private int capacity = Integer.MAX_VALUE;
//提供了两个构造函数,一个是默认构造函数,一个是带有容量参数的构造函数。在构造函数中,
// 使用了初始化代码块,将 `tail.next` 指向 `head`,以形成循环链表结构。
//构造方法中重复的代码可以抽取到初始化代码块里
{
tail.next = head;
}
public LinkedListQueue(int capacity) {
this.capacity = capacity;
// tail.next = head;
}
public LinkedListQueue() {
// tail.next = head;
}
/**
* 向队列尾插入值
* @param value 待插入值
* @return 插入成功返回 true, 插入失败返回 false
* 思路:向队列尾部插入一个元素。如果队列已满(达到容量上限),
* 则返回 false,否则创建一个新的节点并插入到尾部,并更新 tail 的引用,
* 同时增加 size 的计数。返回 true 表示插入成功。
*/
@Override
public boolean offer(E value) {
if (isFull()){
return false;
}
Node<E> added = new Node<>(value,head);
tail.next = added;
tail = added;
size++;
return true;
}
/**
* 从对列头获取值, 并移除. 有的习惯命名为 dequeue
* @return 如果队列非空返回对头值, 否则返回 null
* 思路:从队列头部移除并返回一个元素。如果队列为空,
* 返回 null。将头节点的下一个节点作为新的头节点,并更新 tail 的引用。
* 同时减少 size 的计数。
*/
@Override
public E poll() {
if (isEmpty()){
return null;
}
Node<E> first = head.next;
head.next = first.next;
if (first == tail){
tail = head;
}
size--;
return first.value;
}
/**
* 从对列头获取值, 不移除
* @return 如果队列非空返回对头值, 否则返回 null
* 思路:返回队列头部的元素,但不移除它。如果队列为空,返回 null。
*/
@Override
public E peek() {
if (isEmpty()){
return null;
}
return head.next.value;
}
/**
* 检查队列是否为空
* @return 空返回 true, 否则返回 false
* 思路:判断队列是否为空,即判断头节点和尾节点是否相同。
*/
@Override
public boolean isEmpty() {
return head == tail;
}
/**
* 检查队列是否已满
* @return 满返回 true, 否则返回 false
* 思路:判断队列是否已满,即判断队列的大小是否达到容量上限。
*/
@Override
public boolean isFull() {
return size == capacity;
}
/**
* 实现了 Iterable<E> 接口,返回一个迭代器,用于遍历队列中的元素。
* 迭代器通过 p 指针依次访问每个节点,并返回节点的值。
* @return
*/
@Override
public Iterator<E> iterator() {
return new Iterator<E>() {
Node<E> p = head.next;
@Override
public boolean hasNext() {
return p!=head;
}
@Override
public E next() {
E value = p.value;
p = p.next;
return value;
}
};
}
}
该实现的优点包括:
- 线程安全性:在单线程环境下,该实现是线程安全的,因为没有涉及多线程操作。
- 动态容量:队列的容量可以通过构造函数进行设置,允许动态调整队列的大小。
- 迭代遍历:提供了迭代器接口,可以方便地遍历队列中的元素。
然而,该实现也存在一些潜在的问题:
- 性能:在
poll()
操作中,移除头节点时需要更新头节点和尾节点的引用,可能需要遍历链表来查找下一个节点,可能会影响性能,特别是当队列较长时。 - 容量限制:队列的容量由整数类型
int
表示,受限于int
的取值范围,可能存在容量不足的限制。 - 空指针异常:在迭代器的
next()
方法中,没有对p
的引用进行空指针检查,如果迭代到最后一个节点后继续调用next()
,可能会导致空指针异常。
以上潜在问题如何解决?
- 性能问题:为了提高性能,可以考虑在队列中维护一个指向尾节点的引用,而不是每次都遍历链表查找尾节点。这样,在插入操作中就可以直接访问尾节点并进行插入操作,而不需要遍历整个链表。同时,还可以记录队列的长度,避免在需要计算队列长度时进行遍历。
- 容量限制问题:如果需要支持更大的容量,可以将队列的容量由
int
类型改为long
类型,以扩展容量的取值范围。 - 空指针异常问题:在迭代器的
next()
方法中,需要添加对p
引用的空指针检查,当p
为null
时,抛出异常或者返回一个特定值,以避免空指针异常的发生。
3、环形数组实现
3.1好处
- 对比普通数组,起点和终点更为自由,不用考虑数据移动
- “环”意味着不会存在【越界】问题
- 数组性能更佳
- 环形数组比较适合实现有界队列、RingBuffer 等
3.2下标计算
例如,数组长度是 5,当前位置是 3 ,向前走 2 步,此时下标为 ( 3 + 2 ) % 5 = 0 (3 + 2)\%5 = 0 (3+2)%5=0
( c u r + s t e p ) % l e n g t h (cur + step) \% length (cur+step)%length
- cur 当前指针位置
- step 前进步数
- length 数组长度
注意:
- 如果 step = 1,也就是一次走一步,可以在 >= length 时重置为 0 即可
判断空
当head == tail时,队列为空。
判断满
这里需要留一个空位,不存储元素专门留给尾指针,防止head == tail的情况。
常用:(tail + 1) % array.length == head判满
满之后的策略可以根据业务需求决定
- 例如我们要实现的环形队列,满之后就拒绝入队
3.3判断空、满方法1
仅用 head, tail 判断空满, head, tail 即为索引值, tail 停下来的位置不存储元素
/**
* 仅用 head, tail 判断空满, head, tail 即为索引值, tail 停下来的位置不存储元素
*
* @param <E> 队列中元素类型
*/
public class ArrayQueue1<E> implements Queue<E>, Iterable<E> {
private final E[] array;
private final int length;
//使用两个索引 head 和 tail 来判断队列的空和满,并控制元素的插入和移除。
private int head = 0;
private int tail = 0;
//不想显示警告可以加@SuppressWarnings注解
@SuppressWarnings("all")
public ArrayQueue1(int capacity) {
//在构造方法中,通过创建一个容量为 capacity + 1 的数组来存储元素,数组大小比队列容量大 1,
// 这是为了实现循环队列,空余一个位置来区分队列的空和满。
length = capacity + 1;
array = (E[]) new Object[length];
}
//在插入元素时,首先检查队列是否已满,若满则返回 false,
// 否则将元素存储到 tail 的位置,并更新 tail 的值,使用模运算确保索引在数组范围内循环。
@Override
public boolean offer(E value) {
if (isFull()) {
return false;
}
array[tail] = value;
tail = (tail + 1) % array.length;
return true;
}
//在移除元素时,首先检查队列是否为空,若空则返回 null,否则获取 head 位置的元素,
// 并更新 head 的值,使用模运算确保索引在数组范围内循环。
@Override
public E poll() {
if (isEmpty()) {
return null;
}
E value = array[head];
head = (head + 1) % array.length;
return value;
}
@Override
public E peek() {
if (isEmpty()) {
return null;
}
return array[head];
}
//判断队列是否为空的条件是 head == tail,
// head 和 tail 相等时表示没有元素在队列中。
@Override
public boolean isEmpty() {
return head == tail;
}
//判断队列是否满的条件是 (tail + 1) % array.length == head,
//当 tail 的下一个位置与 head 相等时表示队列已满。
@Override
public boolean isFull() {
return (tail + 1) % array.length == head;
}
//实现了迭代器接口,通过迭代器可以遍历队列中的元素,
//迭代器内部使用 p 作为当前索引,初始值为 head,并通过模运算使 p 在数组范围内循环。
@Override
public Iterator<E> iterator() {
return new Iterator<E>() {
int p = head;
@Override
public boolean hasNext() {
return p != tail;
}
@Override
public E next() {
E value = array[p];
p = (this.p + 1) % array.length;
return value;
}
};
}
}
此方法优点是只需要使用头尾指针就可以判空判满,缺点是要留一个位置存尾指针。那么有其他办法判空判满吗?
拓展:
有时候我们知道某些代码是安全的或者确保没有问题的,但编译器仍然会产生警告。为了消除这些警告信息,可以使用 @SuppressWarnings
注解来告诉编译器忽略特定类型的警告。
@SuppressWarnings("all")
注解用于告诉编译器忽略所有类型的警告信息
3.4判断空、满方法2
用 size 辅助判断空满
/**
* 用 size 辅助判断空满
*
* @param <E> 队列中元素类型
*/
public class ArrayQueue2<E> implements Queue<E>, Iterable<E> {
private final E[] array;
private int head = 0;
private int tail = 0;
private int size = 0; // 队列中元素的个数。
@SuppressWarnings("all")
public ArrayQueue2(int capacity) {
array = (E[]) new Object[capacity];
}
@Override
public boolean offer(E value) {
if (isFull()) {
return false;
}
array[tail] = value;
tail = (tail + 1) % array.length;
size++;
return true;
}
@Override
public E poll() {
if (isEmpty()) {
return null;
}
E value = array[head];
array[head] = null; // help GC
head = (head + 1) % array.length;
size--;
return value;
}
@Override
public E peek() {
if (isEmpty()) {
return null;
}
return array[head];
}
@Override
public boolean isEmpty() {
return size == 0;
}
@Override
public boolean isFull() {
return size == array.length;
}
@Override
public Iterator<E> iterator() {
return new Iterator<E>() {
int p = head;
int count = 0;
@Override
public boolean hasNext() {
return count < size;
}
@Override
public E next() {
E value = array[p];
p = (p + 1) % array.length;
count++;
return value;
}
};
}
}
该实现通过使用 size
变量来辅助判断队列的空满状态,而不再依赖 head
和 tail
的比较。这样可以减少比较操作,提高性能。
需要注意的是,在 poll()
方法中,取出元素后将头部元素置为 null,帮助垃圾回收,以避免内存泄漏。这是一种良好的实践,尤其是当队列中的元素是对象时。
3.5判断空、满方法3
方法一中head和tail是作为索引值。我们能不能转变一下思路,不让他们存储计算结果,单纯作为不断递增的指针值(整数)。用到索引时,再用它们进行计算。例如:
head 和 tail会不断递增,如果自增操作正整数最大值会怎么样?
@Test
public void boundary() {
ArrayQueue3<Integer> queue = new ArrayQueue3<>(10);
// 2147483647 正整数的最大值 int
queue.head = 2147483640;
queue.tail = queue.head;
for (int i = 0; i < 10; i++) {
System.out.println(queue.tail + " " + queue.tail%10);
queue.offer(i);
}
}
发现在索引-8的时候报错了,为什么是-8呢?
再加入第9个数时超过了正整数的最大值,就由正变负了。
解决:
C 语言 改成unsigned int 0~2^32-1
Java 没有无符号整数,可以使用Integer.toUnsignedLong(),将负数转化为更大类型Long
@Test
public void boundary() {
ArrayQueue3<Integer> queue = new ArrayQueue3<>(10);
// 2147483647 正整数的最大值 int
queue.head = 2147483640;
queue.tail = queue.head;
for (int i = 0; i < 10; i++) {
//System.out.println(queue.tail + " " + queue.tail%10);
System.out.println(Integer.toUnsignedLong(queue.tail) + " " + Integer.toUnsignedLong(queue.tail) % 10);
queue.tail++;
// queue.offer(i);
}
}
此时就不会报错了,一般业务都不会超过Long,如果超过就是用更大的类型。
对于以下求模运算我们可以发现一个规律。
如果除数是 2 的 n 次方,那么被除数的后 n 位即为余数 (模)。原理是右移,当除2时右移一位,除8右移三位。
那么我们怎么求被除数的后n位呢?
我们可以发现余数正好为2^n-1 按位与
好处:
-
按位与运算要比求模运算性能更高。
-
不用考虑符号位的问题,只找后几位。
在判满那个方法中tail - head == array.length会出现非法情况吗?
不会,因为数组尾部减数组头部不会超过最大值。
@Override
public boolean isFull() {
return tail - head == array.length;
}
@Test
public void test2() {
int head = 2147483640;
int tail = 2147483647;
tail += 5;
System.out.println(tail);
System.out.println(tail - head);
}
前面可以按位与的前提是除数是2的n次方。如果传入的不是2的n次方该怎么办?
解决:
1、抛异常
2、改成2^n 13->16 22->32
- 求离c最近,比c大的2^n(方法一)
/*
2^4 = 16
2^4.? = 30
2^5 = 32
(int)log2(30) + 1
2
log2(x) = log10(x) / log10(2)
1
10 2^1
100 2^2
1000 2^3
*/
int n = (int) (Math.log10(c-1) / Math.log10(2)) + 1;
System.out.println(n);
System.out.println(1 << n);
-
求离c最近,比c大的2^n(方法二)
c–;
c |= c >> 1;
c |= c >> 2;
c |= c >> 4;
c |= c >> 8;
c |= c >> 16;
c++;
代码如下:
public class ArrayQueue3<E> implements Queue<E>, Iterable<E> {
private final E[] array;
int head = 0;
int tail = 0;
/*
求模运算:
- 如果除数是 2 的 n 次方
- 那么被除数的后 n 位即为余数 (模)
- 求被除数的后 n 位方法: 与 2^n-1 按位与
*/
/*
@SuppressWarnings("all")
public ArrayQueue3(int capacity) {
array = (E[]) new Object[capacity];
}
*/
@SuppressWarnings("all")
public ArrayQueue3(int c) {
/* // 1. 判断容量是否为2的幂,如果不是,则抛出异常。
if ((capacity & capacity - 1) != 0) {
throw new IllegalArgumentException("capacity 必须是2的幂");
}*/
//2、修改容量:将传入的容量 c 减去1,然后通过位运算将其转换为大于等于 c 的最小的2的幂次方值。
//将 c 减去1,例如:13 - 1 = 12。
c -= 1;
//右移1位并与原值按位或运算,例如:12 | 6 = 14。
c |= c >> 1;
//右移2位并与原值按位或运算,例如:14 | 3 = 15。
c |= c >> 2;
//右移4位并与原值按位或运算,例如:15 | 0 = 15。
c |= c >> 4;
//右移8位并与原值按位或运算,例如:15 | 0 = 15。
c |= c >> 8;
//右移16位并与原值按位或运算,例如:15 | 0 = 15。
c |= c >> 16;
//将结果加1,例如:15 + 1 = 16。
c += 1;
//创建数组:使用经过计算后的容量 c 创建泛型数组,并将其赋值给 array。
array = (E[]) new Object[c];
}
/*
head = 0
tail = 3 % 3 = 0
capacity=3
0 1 2
d b c
*/
@Override
public boolean offer(E value) {
if (isFull()) {
return false;
}
//array[tail % array.length] = value;
//array[(int) (Integer.toUnsignedLong(tail) % array.length)] = value;
array[tail & (array.length - 1)] = value;
tail++;
return true;
}
@Override
public E poll() {
if (isEmpty()) {
return null;
}
//E value = array[array.length];
int idx = head & (array.length - 1);
E value = array[idx];
array[idx] = null; // help GC
head++;
return value;
}
@Override
public E peek() {
if (isEmpty()) {
return null;
}
//return array[head % array.length];
return array[head & (array.length - 1)];
}
@Override
public boolean isEmpty() {
return head == tail;
}
@Override
public boolean isFull() {
return tail - head == array.length;
}
@Override
public Iterator<E> iterator() {
return new Iterator<E>() {
int p = head;
@Override
public boolean hasNext() {
return p != tail;
}
@Override
public E next() {
//E value = array[p % array.length];
E value = array[p & (array.length - 1)];
p++;
return value;
}
};
}
public static void main(String[] args) {
// 验证 tail - head 不会有问题
System.out.println(Integer.MAX_VALUE); //2147483647为正整数最大值
// tail 已经自增为负数
int head = 1_900_000_000;
int tail = 2_100_000_000;
for (int i = 0; i < 20; i++) {
tail += 100_000_000;
System.out.println(Integer.toUnsignedLong(tail) + " " + Integer.toUnsignedLong(head) + " " + (tail - head));
}
// 最后一次显示负数是因为 tail-head 4100000000-1900000000=2200000000 也超过了正整数最大值,而实际这种情况不可能发生(数组最大长度为正整数最大值)
// tail 和 tail 都成了负数
System.out.println("===========================");
head = -2094967296; // 2200000000
tail = -2094967296; // 2200000000
for (int i = 0; i < 20; i++) {
tail += 100_000_000;
System.out.println(Integer.toUnsignedLong(tail) + " " + Integer.toUnsignedLong(head) + " " + (tail - head));
}
// 求离c最近,比c大的 2^n (方法1)
int c = 32;
/*
2^4 = 16
2^4.? = 30
2^5 = 32
(int)log2(30) + 1
2
log2(x) = log10(x) / log10(2)
1
10 2^1
100 2^2
1000 2^3
*/
/*1、int n = (int) (Math.log10(c-1) / Math.log10(2)) + 1;
System.out.println(n);
System.out.println(1 << n);*/
//2、 求离c最近,比c大的 2^n (方法2)
c--;
c |= c >> 1;
c |= c >> 2;
c |= c >> 4;
c |= c >> 8;
c |= c >> 16;
c++;
System.out.println(c);
}
}
二、练习
E01. 二叉树层序遍历-力扣 102 题
给你二叉树的根节点 root
,返回其节点值的 层序遍历 。 (即逐层地,从左到右访问所有节点)。
层序遍历一个二叉树。就是从左到右一层一层的去遍历二叉树。
队列先进先出,符合一层一层遍历的逻辑,而栈先进后出适合模拟深度优先遍历也就是递归的逻辑。
其实层序遍历方式就是图论中的广度优先遍历,只不过我们应用在二叉树上。
迭代实现
思路:
我们把每层遍历到的节点都放入到一个结果集中,最后返回这个结果集就可以了。
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
//首先判断根节点是否为空,如果为空,则直接返回一个空的二维列表。
if(root==null) {
return new ArrayList<List<Integer>>();
}
//创建一个二维列表 res 来保存最终的结果。
List<List<Integer>> res = new ArrayList<List<Integer>>();
//创建一个队列 queue 来进行层序遍历。将根节点添加到队列中。
LinkedList<TreeNode> queue = new LinkedList<TreeNode>();
//将根节点放入队列中,然后不断遍历队列
queue.add(root);
//当队列不为空时,执行以下操作
while(queue.size()>0) {
//获取当前队列的长度,这个长度相当于 当前这一层的节点个数
int size = queue.size();
//创建一个临时列表 tmp 来存储当前层节点的值。
ArrayList<Integer> tmp = new ArrayList<Integer>();
//将队列中的元素都拿出来(也就是获取这一层的节点),放到临时list中
//如果节点的左/右子树不为空,也放入队列中
for(int i=0;i<size;++i) {
//从队列中依次取出 size 个节点,将它们的值添加到 tmp 列表中,并将它们的左右子节点(如果存在)加入队列中。
TreeNode t = queue.remove();
//
tmp.add(t.val);
if(t.left!=null) {
queue.add(t.left);
}
if(t.right!=null) {
queue.add(t.right);
}
}
//将临时list加入最终返回结果中
res.add(tmp);
}
return res;
}
}
该代码通过 DFS 和 BFS 两种方式实现了二叉树的层序遍历。DFS 方式使用递归进行深度优先搜索,将节点值按层级添加到结果列表中。BFS 方式使用队列进行广度优先搜索,每次处理一层。
讲透二叉树的层序遍历 | 广度优先搜索 | LeetCode:102.二叉树的层序遍历
测试:
时间复杂度: O(n)
空间复杂度: O(n)
递归实现
按照深度优先的处理顺序,会先访问节点 1,再访问节点 2,接着是节点 3。之后是第二列的 4 和 5,最后是第三列的 6。
每次递归的时候都需要带一个 index(表示当前的层数),也就对应那个田字格子中的第几行,如果当前行对应的 list 不存在,就加入一个空 list 进去。
动态演示如下:
class Solution {
public List<List<Integer>> levelOrder(TreeNode root) {
//首先判断根节点是否为空,如果为空,则直接返回一个空的二维列表。
if(root==null) {
return new ArrayList<List<Integer>>();
}
//用来存放最终结果
List<List<Integer>> res = new ArrayList<List<Integer>>();
//调用递归函数 dfs 进行层序遍历,初始层数为1。
dfs(1,root,res);
//递归结束后,返回最终的结果列表 res
return res;
}
void dfs(int index,TreeNode root, List<List<Integer>> res) {
//首先判断如果 res 列表的长度小于当前层数 index,则向 res 列表中添加一个空的列表,该空列表用于存储当前层的节点值。
if(res.size()<index) {
res.add(new ArrayList<Integer>());
}
//将当前节点的值 root.val 加入到 res 列表中对应层数的列表中。
res.get(index-1).add(root.val);
//递归的处理左子树,右子树,同时将层数index+1
if(root.left!=null) {
dfs(index+1, root.left, res);
}
if(root.right!=null) {
dfs(index+1, root.right, res);
}
}
}
时间复杂度:O(N)
空间复杂度:O(h),h
是树的高度
测试:
E02.二叉树的层次遍历 II-力扣107题
思路:
相对于102.二叉树的层序遍历,就是最后把result数组反转一下就可以了。
// 107. 二叉树的层序遍历 II
public class N0107 {
/**
* 解法:队列,迭代。
* 层序遍历,再翻转数组即可。
*/
public List<List<Integer>> solution1(TreeNode root) {
List<List<Integer>> list = new ArrayList<>();
Deque<TreeNode> que = new LinkedList<>();
if (root == null) {
return list;
}
que.offerLast(root);
while (!que.isEmpty()) {
List<Integer> levelList = new ArrayList<>();
int levelSize = que.size();
for (int i = 0; i < levelSize; i++) {
TreeNode peek = que.peekFirst();
levelList.add(que.pollFirst().val);
if (peek.left != null) {
que.offerLast(peek.left);
}
if (peek.right != null) {
que.offerLast(peek.right);
}
}
list.add(levelList);
}
List<List<Integer>> result = new ArrayList<>();
//反转二位列表给result
for (int i = list.size() - 1; i >= 0; i-- ) {
result.add(list.get(i));
}
return result;
}
}
E03.二叉树的右视图-力扣199题
思路:
层序遍历的时候,判断是否遍历到单层的最后面的元素,如果是,就放进result数组中,随后返回result就可以了。
// 199.二叉树的右视图
public class N0199 {
/**
* 解法:队列,迭代。
* 每次返回每层的最后一个字段即可。
*
* 小优化:每层右孩子先入队。代码略。
*/
public List<Integer> rightSideView(TreeNode root) {
List<Integer> list = new ArrayList<>();
Deque<TreeNode> que = new LinkedList<>();
if (root == null) {
return list;
}
que.offerLast(root);
while (!que.isEmpty()) {
int levelSize = que.size();
for (int i = 0; i < levelSize; i++) {
TreeNode poll = que.pollFirst();
if (poll.left != null) {
que.addLast(poll.left);
}
if (poll.right != null) {
que.addLast(poll.right);
}
//判断是否遍历到单层的最后一个元素,是就放进result数组中
if (i == levelSize - 1) {
list.add(poll.val);
}
}
}
return list;
}
}
E04.二叉树的层平均值-力扣637题
思路:
本题就是层序遍历的时候把一层求个总和在取一个均值。
// 637. 二叉树的层平均值
public class N0637 {
/**
* 解法:队列,迭代。
* 每次返回每层的最后一个字段即可。
*/
public List<Double> averageOfLevels(TreeNode root) {
List<Double> list = new ArrayList<>();
Deque<TreeNode> que = new LinkedList<>();
if (root == null) {
return list;
}
que.offerLast(root);
while (!que.isEmpty()) {
TreeNode peek = que.peekFirst();
int levelSize = que.size();
double levelSum = 0.0;
for (int i = 0; i < levelSize; i++) {
TreeNode poll = que.pollFirst();
//求单层的总值
levelSum += poll.val;
if (poll.left != null) {
que.addLast(poll.left);
}
if (poll.right != null) {
que.addLast(poll.right);
}
}
//求单层的平均值
list.add(levelSum / levelSize);
}
return list;
}
}
E05.N叉树的层序遍历 II-力扣429题
思路:
这道题依旧是模板题,只不过一个节点有多个孩子了
// 429. N 叉树的层序遍历
public class N0429 {
/**
* 解法1:队列,迭代。
*/
public List<List<Integer>> levelOrder(Node root) {
//创建一个空的二维列表 list,用于存储最终的结果。
List<List<Integer>> list = new ArrayList<>();
//创建一个双端队列 que,用于进行层序遍历。
Deque<Node> que = new LinkedList<>();
//首先判断根节点 root 是否为空,如果为空,则直接返回空的二维列表。
if (root == null) {
return list;
}
//将根节点 root 加入队列 que。
que.offerLast(root);
//进入循环,直到队列为空。每次循环表示遍历一层节点。
while (!que.isEmpty()) {
//在循环内部,首先获取当前层的节点个数 levelSize,用于控制循环次数。
int levelSize = que.size();
//创建一个空的列表 levelList,用于存储当前层节点的值。
List<Integer> levelList = new ArrayList<>();
//入内层循环,循环 levelSize 次。每次循环从队列 que 中取出一个节点 poll。
for (int i = 0; i < levelSize; i++) {
Node poll = que.pollFirst();
//将节点 poll 的值加入到当前层列表 levelList 中。
levelList.add(poll.val);
// 获取节点 poll 的子节点列表 children,如果子节点列表为空,则跳过本次循环。
List<Node> children = poll.children;
if (children == null || children.size() == 0) {
continue;
}
//遍历子节点列表 children,将非空子节点加入到队列 que 中。
for (Node child : children) {
if (child != null) {
que.offerLast(child);
}
}
}
//内层循环结束后,将当前层列表 levelList 加入到最终结果列表 list 中。
list.add(levelList);
}
//返回最终结果列表 list。
return list;
}
class Node {
public int val;
public List<Node> children;
public Node() {}
public Node(int _val) {
val = _val;
}
public Node(int _val, List<Node> _children) {
val = _val;
children = _children;
}
}
}
E06.在每个树行中找最大值-力扣515题
思路:
层序遍历,取每一层的最大值
class Solution {
public List<Integer> largestValues(TreeNode root) {
if(root == null){
return Collections.emptyList();
}
List<Integer> result = new ArrayList();
Queue<TreeNode> queue = new LinkedList();
queue.offer(root);
while(!queue.isEmpty()){
int max = Integer.MIN_VALUE;
for(int i = queue.size(); i > 0; i--){
TreeNode node = queue.poll();
//取最大值
max = Math.max(max, node.val);
if(node.left != null) queue.offer(node.left);
if(node.right != null) queue.offer(node.right);
}
//结果返回最大值
result.add(max);
}
return result;
}
}
E07.填充每个节点的下一个右侧节点指针-力扣116题
思路:
本题依然是层序遍历,只不过在单层遍历的时候记录一下本层的头部节点,然后在遍历的时候让前一个节点指向本节点就可以了
class Solution {
public Node connect(Node root) {
//创建一个队列 tmpQueue,用于进行层序遍历。
Queue<Node> tmpQueue = new LinkedList<Node>();
//如果根节点 root 不为空,将其加入队列 tmpQueue 中。
if (root != null) tmpQueue.add(root);
//进入循环,直到队列为空。每次循环表示遍历一层节点。
while (tmpQueue.size() != 0){
//在循环内部,首先获取当前层的节点个数 size,用于控制循环次数。
int size = tmpQueue.size();
//从队列 tmpQueue 中取出一个节点 cur,表示当前层的节点。
Node cur = tmpQueue.poll();
//检查节点 cur 的左子节点和右子节点是否存在,如果存在则将它们加入队列 tmpQueue 中。
if (cur.left != null) tmpQueue.add(cur.left);
if (cur.right != null) tmpQueue.add(cur.right);
//进入内层循环,循环 size - 1 次,表示处理当前层节点的连接。
for (int index = 1; index < size; index++){
//从队列 tmpQueue 中取出下一个节点 next。
Node next = tmpQueue.poll();
//检查节点 next 的左子节点和右子节点是否存在,如果存在则将它们加入队列 tmpQueue 中。
if (next.left != null) tmpQueue.add(next.left);
if (next.right != null) tmpQueue.add(next.right);
//将节点 cur 的 next 指针指向节点 next,实现节点之间的连接。
cur.next = next;
//将节点 cur 更新为节点 next,用于下一次循环。
cur = next;
}
//内层循环结束后,完成当前层节点的连接。
}
//外层循环继续,直到遍历完所有层节点。
return root;
}
}
E08.填充每个节点的下一个右侧节点指针II-力扣117题
思路:
这道题目说是二叉树,但116题目说是完整二叉树,其实没有任何差别,一样的代码一样的逻辑一样的味道
// 二叉树之层次遍历
class Solution {
public Node connect(Node root) {
Queue<Node> queue = new LinkedList<>();
if (root != null) {
queue.add(root);
}
while (!queue.isEmpty()) {
int size = queue.size();
Node node = null;
Node nodePre = null;
for (int i = 0; i < size; i++) {
if (i == 0) {
nodePre = queue.poll(); // 取出本层头一个节点
node = nodePre;
} else {
node = queue.poll();
nodePre.next = node; // 本层前一个节点 next 指向当前节点
nodePre = nodePre.next;
}
if (node.left != null) {
queue.add(node.left);
}
if (node.right != null) {
queue.add(node.right);
}
}
nodePre.next = null; // 本层最后一个节点 next 指向 null
}
return root;
}
}
E09.二叉树的最大深度-力扣104题
思路:
使用迭代法的话,使用层序遍历是最为合适的,因为最大的深度就是二叉树的层数,和层序遍历的方式极其吻合。
在二叉树中,一层一层的来遍历二叉树,记录一下遍历的层数就是二叉树的深度,如图所示:
class Solution {
public int maxDepth(TreeNode root) {
if (root == null) return 0;
Queue<TreeNode> que = new LinkedList<>();
que.offer(root);
int depth = 0;
while (!que.isEmpty())
{
int len = que.size();
while (len > 0)
{
TreeNode node = que.poll();
if (node.left != null) que.offer(node.left);
if (node.right != null) que.offer(node.right);
len--;
}
depth++;
}
return depth;
}
}
E10.二叉树的最小深度-力扣111题
相对于 104.二叉树的最大深度 ,本题还也可以使用层序遍历的方式来解决,思路是一样的。
需要注意的是,只有当左右孩子都为空的时候,才说明遍历的最低点了。如果其中一个孩子为空则不是最低点
class Solution {
public int minDepth(TreeNode root){
if (root == null) {
return 0;
}
Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);
int depth = 0;
while (!queue.isEmpty()){
int size = queue.size();
depth++;
TreeNode cur = null;
for (int i = 0; i < size; i++) {
cur = queue.poll();
//如果当前节点的左右孩子都为空,直接返回最小深度
if (cur.left == null && cur.right == null){
return depth;
}
if (cur.left != null) queue.offer(cur.left);
if (cur.right != null) queue.offer(cur.right);
}
}
return depth;
}
}
Ex1. 设计队列-力扣 622 题
基于环形数组的实现:
class MyCircularQueue {
private int front;
private int rear;
private int capacity;
private int[] elements;
public MyCircularQueue(int k) {
capacity = k + 1;
elements = new int[capacity];
rear = front = 0;
}
public boolean enQueue(int value) {
if (isFull()) {
return false;
}
elements[rear] = value;
rear = (rear + 1) % capacity;
return true;
}
public boolean deQueue() {
if (isEmpty()) {
return false;
}
front = (front + 1) % capacity;
return true;
}
public int Front() {
if (isEmpty()) {
return -1;
}
return elements[front];
}
public int Rear() {
if (isEmpty()) {
return -1;
}
return elements[(rear - 1 + capacity) % capacity];
}
public boolean isEmpty() {
return rear == front;
}
public boolean isFull() {
return ((rear + 1) % capacity) == front;
}
}
时间复杂度: 初始化和每项操作的时间复杂度均为 O(1)。
空间复杂度: O(k),其中 k 为给定的队列元素数目。
基于链表的实现:
class MyCircularQueue {
private ListNode head;
private ListNode tail;
private int capacity;
private int size;
public MyCircularQueue(int k) {
capacity = k;
size = 0;
}
public boolean enQueue(int value) {
if (isFull()) {
return false;
}
ListNode node = new ListNode(value);
if (head == null) {
head = tail = node;
} else {
tail.next = node;
tail = node;
}
size++;
return true;
}
public boolean deQueue() {
if (isEmpty()) {
return false;
}
ListNode node = head;
head = head.next;
size--;
return true;
}
public int Front() {
if (isEmpty()) {
return -1;
}
return head.val;
}
public int Rear() {
if (isEmpty()) {
return -1;
}
return tail.val;
}
public boolean isEmpty() {
return size == 0;
}
public boolean isFull() {
return size == capacity;
}
}
时间复杂度: 初始化和每项操作的时间复杂度均为 O(1)。
空间复杂度: O(k),其中 k 为给定的队列元素数目。