数据结构与算法5-队列(内含LeedCode练习)

思考:

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(),可能会导致空指针异常。

以上潜在问题如何解决?

  1. 性能问题:为了提高性能,可以考虑在队列中维护一个指向尾节点的引用,而不是每次都遍历链表查找尾节点。这样,在插入操作中就可以直接访问尾节点并进行插入操作,而不需要遍历整个链表。同时,还可以记录队列的长度,避免在需要计算队列长度时进行遍历。
  2. 容量限制问题:如果需要支持更大的容量,可以将队列的容量由 int 类型改为 long 类型,以扩展容量的取值范围。
  3. 空指针异常问题:在迭代器的 next() 方法中,需要添加对 p 引用的空指针检查,当 pnull 时,抛出异常或者返回一个特定值,以避免空指针异常的发生。

3、环形数组实现

3.1好处
  1. 对比普通数组,起点和终点更为自由,不用考虑数据移动
  2. “环”意味着不会存在【越界】问题
  3. 数组性能更佳
  4. 环形数组比较适合实现有界队列、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 变量来辅助判断队列的空满状态,而不再依赖 headtail 的比较。这样可以减少比较操作,提高性能。

​ 需要注意的是,在 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 题

102. 二叉树的层序遍历 - 力扣(LeetCode)

给你二叉树的根节点 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 为给定的队列元素数目。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值