邂逅Hello算法 第三篇(常见的数据结构)- 双向链表和双端队列

106 阅读17分钟

双向链表

双向链表的查找 和 插入首元素尾元素 和删除中间元素和插入中间元素的动图效果

屏幕录制 2024-08-13 2249 -small-original.gif

简单来说就是链表上多了一个向后连接的last

双端队列

后面介绍了

数组

1. 初始化数组

可以使用 [] 创建一个空数组,也可以在创建时填充初始值:

// 空数组
let arr1 = [];

// 带初始值的数组
let arr2 = [1, 2, 3, 4];

当然也可以var arr = new Array(5).fill(0) 直接创建填充满0的数组

2. 访问元素

数组的元素通过索引访问,索引从 0 开始:

let arr = [10, 20, 30];
console.log(arr[0]); // 输出: 10
console.log(arr[1]); // 输出: 20

我们发现数组首个元素的索引为 0 ,这似乎有些反直觉,因为从 1 开始计数会更自然。但从地址计算公式的角度看,索引本质上是内存地址的偏移量。首个元素的地址偏移量是 0 ,因此它的索引为 0 是合理的。

3. 插入元素

可以使用 push() 方法在数组末尾插入元素,或者使用 unshift() 方法在数组开头插入元素:

let arr = [1, 2, 3];

// 在末尾插入
arr.push(4); // [1, 2, 3, 4]

// 在开头插入
arr.unshift(0); // [0, 1, 2, 3, 4]

push(),unshift()两种方法

image.png

image.png push()我比较熟悉,unshift()不太熟悉。就是在开头插入,因为JS中数组是动态的,自动添加了一位,在Java中数组就不能这样搞,只有ArrayList能这样玩。

当然我们还可以用splice插入元素。

假设我们有一个数组 [1, 2, 4, 5],我们想在索引 2 的位置插入元素 3,可以这样做:

let arr = [1, 2, 4, 5];

// 使用 splice() 插入元素
arr.splice(2, 0, 3); // 第一个参数是插入位置,第二个参数是删除元素个数,第三个参数是要插入的元素

console.log(arr); // 输出: [1, 2, 3, 4, 5]

这个是我没想到的,感觉是JS独有的方法。

4. 删除元素

可以使用 pop() 方法删除数组末尾的元素,使用 shift() 方法删除数组开头的元素,或者使用 splice() 方法删除指定位置的元素:

let arr = [1, 2, 3, 4];

// 删除末尾元素
arr.pop(); // [1, 2, 3]

// 删除开头元素
arr.shift(); // [2, 3]

// 删除指定位置的元素
arr.splice(1, 1); // 从索引1开始删除1个元素,结果: [2]

如上插入元素

5. 遍历数组

可以使用 for 循环、forEach() 方法或其他数组遍历方法:

let arr = [1, 2, 3, 4];

// 使用 for 循环
for (let i = 0; i < arr.length; i++) {
    console.log(arr[i]);
}

// 使用 forEach()
arr.forEach(element => console.log(element));

这里可以复习一个知识点。for of ; for in 和 forEach 和 for的区别

1. for...of

for...of 用于遍历可迭代对象(如数组、字符串、Set、Map等)的元素。它直接访问元素值。

let arr = [1, 2, 3];

for (const value of arr) {
    console.log(value); // 输出: 1, 2, 3
}
  • 适用场景:适用于遍历数组、字符串等可迭代对象的元素。
  • 优点:简洁明了,适合直接操作元素值。
2. for...in

for...in 用于遍历对象的属性(包括继承的属性)。对于数组,它会遍历数组的索引。

let obj = { a: 1, b: 2, c: 3 };

for (const key in obj) {
    console.log(key, obj[key]); // 输出: a 1, b 2, c 3
}

let arr = [10, 20, 30];

for (const index in arr) {
    console.log(index, arr[index]); // 输出: 0 10, 1 20, 2 30
}
  • 适用场景:适用于遍历对象的属性(不推荐用于数组)。
  • 优点:可以遍历对象的所有属性,包括继承的属性。
3. forEach()

forEach() 是数组的方法,用于遍历数组的元素。它接收一个回调函数,该函数会在每个元素上调用。

let arr = [1, 2, 3];

arr.forEach((value, index) => {
    console.log(index, value); // 输出: 0 1, 1 2, 2 3
});
  • 适用场景:适用于数组,且当你需要在遍历时执行特定操作时。
  • 优点:语法简洁,支持函数式编程风格。不会中途退出遍历。
4. for

for 循环是最基本的循环结构,可以用于遍历数组、对象、数字等。

let arr = [1, 2, 3];

for (let i = 0; i < arr.length; i++) {
    console.log(arr[i]); // 输出: 1, 2, 3
}
  • 适用场景:适用于任何需要循环的情况,包括数组、对象、数字等。
  • 优点:灵活强大,可以用来遍历各种结构,适合需要控制循环流程的情况。

当然除了这些以外还有比较特殊用法的

1. map()

map() 方法创建一个新数组,其结果是通过调用提供的函数处理数组的每个元素得来的。

let arr = [1, 2, 3];
let newArr = arr.map(value => value * 2);

console.log(newArr); // 输出: [2, 4, 6]
  • 适用场景:当你需要对数组的每个元素进行操作并生成新数组时。
2. filter()

filter() 方法创建一个新数组,其中包含所有通过测试的元素。

let arr = [1, 2, 3, 4];
let filteredArr = arr.filter(value => value > 2);

console.log(filteredArr); // 输出: [3, 4]
  • 适用场景:当你需要从数组中筛选出符合特定条件的元素时。
3. reduce()

reduce() 方法对数组中的每个元素执行指定的函数,并将结果汇总为单个值。

let arr = [1, 2, 3, 4];
let sum = arr.reduce((accumulator, currentValue) => accumulator + currentValue, 0);

console.log(sum); // 输出: 10
  • 适用场景:当你需要对数组中的元素进行累积计算时,比如求和、求最大值等。
4. some()

some() 方法测试数组中的某些元素是否通过了指定的测试函数。

let arr = [1, 2, 3, 4];
let hasEven = arr.some(value => value % 2 === 0);

console.log(hasEven); // 输出: true
  • 适用场景:当你需要检查数组中是否有元素满足某个条件时。
5. every()

every() 方法测试数组中的所有元素是否都通过了指定的测试函数。

let arr = [2, 4, 6, 8];
let allEven = arr.every(value => value % 2 === 0);

console.log(allEven); // 输出: true
  • 适用场景:当你需要检查数组中的所有元素是否都满足某个条件时。
6. find()

find() 方法返回数组中满足测试函数的第一个元素,如果没有找到则返回 undefined

let arr = [5, 12, 8, 130, 44];
let found = arr.find(value => value > 10);

console.log(found); // 输出: 12
  • 适用场景:当你需要找到数组中第一个满足条件的元素时。
7. findIndex()

findIndex() 方法返回数组中满足测试函数的第一个元素的索引,如果没有找到则返回 -1

let arr = [5, 12, 8, 130, 44];
let index = arr.findIndex(value => value > 10);

console.log(index); // 输出: 1
  • 适用场景:当你需要找到数组中第一个满足条件的元素的索引时。
8. forEachRight()

forEachRight() 是一些库(如 Lodash)提供的反向遍历数组的方法,JavaScript 原生没有。它的作用是从数组的最后一个元素开始向前遍历。

// Lodash 示例
_.forEachRight([1, 2, 3], function(value) {
  console.log(value); // 输出: 3, 2, 1
});
  • 适用场景:当你需要从数组的末尾开始遍历时(需要借助外部库)。

6. 查找元素

可以使用 indexOf() 查找元素的位置,或者使用 find() 查找满足条件的元素:

let arr = [10, 20, 30];

// 查找位置
let index = arr.indexOf(20); // 1

// 查找元素
let found = arr.find(element => element > 15); // 20

其实除了这些定义好的方法以外,还能自己定义去查找元素。

7. 扩容数组

JavaScript 数组的大小是动态的,不需要手动扩容。当数组达到容量限制时,JavaScript 会自动扩展数组的大小。

let arr = [1, 2, 3];
arr.length = 10; // 扩容数组,但原有元素不会改变
console.log(arr); // [1, 2, 3, <7 empty items>]

8. 排序列表

nums.sort((a, b) => a - b); // 排序后,列表元素从小到大排列

链表

//链表.js
class ListNode {
    constructor(value = null, next = null) {
        this.value = value;
        this.next = next;
    }
}
//初始化链表

class LinkedList {
    constructor() {
        this.head = null;
        this.tail = null;
        this.size = 0;
    }
    //构造函数初始化

    append(value) {
        const newNode = new ListNode(value);
        if (this.head === null) {
            this.head = newNode;
            this.tail = newNode;
        } else {
            this.tail.next = newNode;
            this.tail = newNode;
        }
        this.size++;
    }
    //添加节点

    prepend(value) {
        const newNode = new ListNode(value, this.head);
        this.head = newNode;
        if (this.size === 0) {
            this.tail = newNode;
        }
        this.size++;
    }

    insertAt(value, index) {
        if (index < 0 || index > this.size) {
            throw new Error('Index out of bounds');
        }
        if (index === 0) {
            this.prepend(value);
        } else if (index === this.size) {
            this.append(value);
        } else {
            const newNode = new ListNode(value);
            let current = this.head;
            for (let i = 0; i < index - 1; i++) {
                current = current.next;
            }
            newNode.next = current.next;
            current.next = newNode;
            this.size++;
        }
    }
    //插入节点

    remove(value) {
        if (this.size === 0) return;

        if (this.head.value === value) {
            this.head = this.head.next;
            if (this.size === 1) {
                this.tail = null;
            }
            this.size--;
            return;
        }
   

        let current = this.head;
        while (current.next !== null && current.next.value !== value) {
            current = current.next;
        }

        if (current.next !== null) {
            current.next = current.next.next;
            if (current.next === null) {
                this.tail = current;
            }
            this.size--;
        }
    }
     //移除节点

    removeAt(index) {
        if (index < 0 || index >= this.size) {
            throw new Error('Index out of bounds');
        }
        if (index === 0) {
            this.head = this.head.next;
            if (this.size === 1) {
                this.tail = null;
            }
            this.size--;
            return;
        }
        let current = this.head;
        for (let i = 0; i < index - 1; i++) {
            current = current.next;
        }
        current.next = current.next.next;
        if (current.next === null) {
            this.tail = current;
        }
        this.size--;
    }
    //移除目标节点

    find(value) {
        let current = this.head;
        while (current !== null) {
            if (current.value === value) {
                return current;
            }
            current = current.next;
        }
        return null;
    }
    //查找

    getAt(index) {
        if (index < 0 || index >= this.size) {
            throw new Error('Index out of bounds');
        }
        let current = this.head;
        for (let i = 0; i < index; i++) {
            current = current.next;
        }
        return current;
    }
    //拿到下标的

    printList() {
        let current = this.head;
        let result = [];
        while (current !== null) {
            result.push(current.value);
            current = current.next;
        }
        return result;
    }
    //打印链表
}

export { LinkedList };

链表的区分

  1. 单向链表
  2. 双向链表
  3. 环形链表 单向链表通常用于实现栈、队列、哈希表和图等数据结构。
  • 栈与队列:当插入和删除操作都在链表的一端进行时,它表现的特性为先进后出,对应栈;当插入操作在链表的一端进行,删除操作在链表的另一端进行,它表现的特性为先进先出,对应队列。
  • 哈希表:链式地址是解决哈希冲突的主流方案之一,在该方案中,所有冲突的元素都会被放到一个链表中。
  • :邻接表是表示图的一种常用方式,其中图的每个顶点都与一个链表相关联,链表中的每个元素都代表与该顶点相连的其他顶点。

双向链表常用于需要快速查找前一个和后一个元素的场景。

  • 高级数据结构:比如在红黑树、B 树中,我们需要访问节点的父节点,这可以通过在节点中保存一个指向父节点的引用来实现,类似于双向链表。
  • 浏览器历史:在网页浏览器中,当用户点击前进或后退按钮时,浏览器需要知道用户访问过的前一个和后一个网页。双向链表的特性使得这种操作变得简单。
  • LRU 算法:在缓存淘汰(LRU)算法中,我们需要快速找到最近最少使用的数据,以及支持快速添加和删除节点。这时候使用双向链表就非常合适。

环形链表常用于需要周期性操作的场景,比如操作系统的资源调度。

  • 时间片轮转调度算法:在操作系统中,时间片轮转调度算法是一种常见的 CPU 调度算法,它需要对一组进程进行循环。每个进程被赋予一个时间片,当时间片用完时,CPU 将切换到下一个进程。这种循环操作可以通过环形链表来实现。
  • 数据缓冲区:在某些数据缓冲区的实现中,也可能会使用环形链表。比如在音频、视频播放器中,数据流可能会被分成多个缓冲块并放入一个环形链表,以便实现无缝播放。

栈(Stack)是一种数据结构,用于管理和存储数据,其核心操作基于“后进先出”(LIFO, Last In, First Out)的原则。栈的基本操作包括入栈(push)、出栈(pop)和查看栈顶元素(peek)。

栈的基本概念

  1. 后进先出(LIFO) :在栈中,最新放入的元素最先被移除。可以想象成一堆盘子,最后放上去的盘子必须最先取走。

  2. 基本操作

    • 入栈(push) :将一个元素添加到栈顶。
    • 出栈(pop) :移除并返回栈顶的元素。
    • 查看栈顶元素(peek) :返回栈顶的元素但不移除它。
    • 检查栈是否为空(isEmpty) :检查栈中是否还有元素。

栈的实现

栈可以使用数组或链表来实现。下面是用 JavaScript 实现栈的两种方式:数组和链表。

使用数组实现栈

在 JavaScript 中,数组已经提供了栈的基本操作:

class Stack {
    constructor() {
        this.items = []; // 用于存储栈的元素
    }

    // 入栈操作
    push(element) {
        this.items.push(element);
    }

    // 出栈操作
    pop() {
        if (this.isEmpty()) {
            return 'Stack is empty'; // 如果栈为空,返回提示
        }
        return this.items.pop();
    }

    // 查看栈顶元素
    peek() {
        if (this.isEmpty()) {
            return 'Stack is empty'; // 如果栈为空,返回提示
        }
        return this.items[this.items.length - 1];
    }

    // 检查栈是否为空
    isEmpty() {
        return this.items.length === 0;
    }

    // 获取栈的大小
    size() {
        return this.items.length;
    }

    // 清空栈
    clear() {
        this.items = [];
    }
}

// 示例
const stack = new Stack();
stack.push(1);
stack.push(2);
console.log(stack.peek()); // 输出: 2
console.log(stack.pop()); // 输出: 2
console.log(stack.peek()); // 输出: 1
console.log(stack.isEmpty()); // 输出: false
console.log(stack.size()); // 输出: 1
使用链表实现栈

使用链表实现栈的好处是能高效地进行入栈和出栈操作。下面是链表实现栈的代码:

class ListNode {
    constructor(value = null, next = null) {
        this.value = value;
        this.next = next;
    }
}

class Stack {
    constructor() {
        this.top = null; // 栈顶元素
        this.size = 0; // 栈的大小
    }

    // 入栈操作
    push(value) {
        this.top = new ListNode(value, this.top);
        this.size++;
    }

    // 出栈操作
    pop() {
        if (this.isEmpty()) {
            return 'Stack is empty'; // 如果栈为空,返回提示
        }
        const value = this.top.value;
        this.top = this.top.next;
        this.size--;
        return value;
    }

    // 查看栈顶元素
    peek() {
        if (this.isEmpty()) {
            return 'Stack is empty'; // 如果栈为空,返回提示
        }
        return this.top.value;
    }

    // 检查栈是否为空
    isEmpty() {
        return this.size === 0;
    }

    // 获取栈的大小
    size() {
        return this.size;
    }

    // 清空栈
    clear() {
        this.top = null;
        this.size = 0;
    }
}

// 示例
const stack = new Stack();
stack.push(1);
stack.push(2);
console.log(stack.peek()); // 输出: 2
console.log(stack.pop()); // 输出: 2
console.log(stack.peek()); // 输出: 1
console.log(stack.isEmpty()); // 输出: false
console.log(stack.size()); // 输出: 1

栈的应用场景

栈在计算机科学中有广泛的应用,包括:

  1. 函数调用管理:函数调用栈用于跟踪函数调用及返回。
  2. 表达式求值:使用栈来处理数学表达式(如中缀表达式转后缀表达式)。
  3. 回溯算法:在解决问题时,栈用于记录状态,方便回退(如迷宫问题)。
  4. 撤销操作:在文本编辑器中,栈用于实现撤销和重做功能。

队列

队列(Queue)是一种数据结构,用于管理和存储数据,其核心操作基于“先进先出”(FIFO, First In, First Out)的原则。队列中的数据项按照它们被插入的顺序排列,最早插入的数据项会被最先移除。

队列的基本概念

  1. 先进先出(FIFO) :在队列中,最早插入的元素最先被移除。可以想象成排队等候的人,最早到达的人最早被服务。

  2. 基本操作

    • 入队(enqueue) :将一个元素添加到队列的末尾。
    • 出队(dequeue) :移除并返回队列的前端元素。
    • 查看队列前端元素(peek/front) :返回队列前端的元素但不移除它。
    • 检查队列是否为空(isEmpty) :检查队列中是否还有元素。

队列的实现

队列可以使用数组或链表来实现。下面是用 JavaScript 实现队列的两种方式:数组和链表。

使用数组实现队列

在 JavaScript 中,数组提供了实现队列的基本方法,如 push()shift()push() 用于入队,shift() 用于出队。

class Queue {
    constructor() {
        this.items = []; // 用于存储队列的元素
    }

    // 入队操作
    enqueue(element) {
        this.items.push(element);
    }

    // 出队操作
    dequeue() {
        if (this.isEmpty()) {
            return 'Queue is empty'; // 如果队列为空,返回提示
        }
        return this.items.shift();
    }

    // 查看队列前端元素
    front() {
        if (this.isEmpty()) {
            return 'Queue is empty'; // 如果队列为空,返回提示
        }
        return this.items[0];
    }

    // 检查队列是否为空
    isEmpty() {
        return this.items.length === 0;
    }

    // 获取队列的大小
    size() {
        return this.items.length;
    }

    // 清空队列
    clear() {
        this.items = [];
    }
}

// 示例
const queue = new Queue();
queue.enqueue(1);
queue.enqueue(2);
console.log(queue.front()); // 输出: 1
console.log(queue.dequeue()); // 输出: 1
console.log(queue.front()); // 输出: 2
console.log(queue.isEmpty()); // 输出: false
console.log(queue.size()); // 输出: 1
使用链表实现队列

使用链表实现队列的好处是能高效地进行入队和出队操作。下面是链表实现队列的代码:

class ListNode {
    constructor(value = null, next = null) {
        this.value = value;
        this.next = next;
    }
}

class Queue {
    constructor() {
        this.front = null; // 队列前端元素
        this.rear = null; // 队列末尾元素
        this.size = 0; // 队列的大小
    }

    // 入队操作
    enqueue(value) {
        const newNode = new ListNode(value);
        if (this.isEmpty()) {
            this.front = newNode;
            this.rear = newNode;
        } else {
            this.rear.next = newNode;
            this.rear = newNode;
        }
        this.size++;
    }

    // 出队操作
    dequeue() {
        if (this.isEmpty()) {
            return 'Queue is empty'; // 如果队列为空,返回提示
        }
        const value = this.front.value;
        this.front = this.front.next;
        if (this.front === null) {
            this.rear = null;
        }
        this.size--;
        return value;
    }

    // 查看队列前端元素
    front() {
        if (this.isEmpty()) {
            return 'Queue is empty'; // 如果队列为空,返回提示
        }
        return this.front.value;
    }

    // 检查队列是否为空
    isEmpty() {
        return this.size === 0;
    }

    // 获取队列的大小
    size() {
        return this.size;
    }

    // 清空队列
    clear() {
        this.front = null;
        this.rear = null;
        this.size = 0;
    }
}

// 示例
const queue = new Queue();
queue.enqueue(1);
queue.enqueue(2);
console.log(queue.front()); // 输出: 1
console.log(queue.dequeue()); // 输出: 1
console.log(queue.front()); // 输出: 2
console.log(queue.isEmpty()); // 输出: false
console.log(queue.size()); // 输出: 1

队列的应用场景

队列在计算机科学中有广泛的应用,包括:

  1. 任务调度:操作系统中的任务调度程序使用队列来管理进程或线程。
  2. 打印任务管理:打印队列用于管理多个打印任务的顺序。
  3. 消息队列:在消息传递系统中,队列用于处理消息的发送和接收。
  4. 广度优先搜索(BFS) :在图的广度优先搜索算法中使用队列来管理待访问的节点。

当然这些都是最基本的队列还有优先队列、双端队列、循环队列和单调队列

1. 优先队列(Priority Queue)

概念: 优先队列是一种特殊类型的队列,其中每个元素都有一个优先级。元素按照优先级顺序被移除,而不是按照插入的顺序。具有高优先级的元素在队列中排在前面,优先被处理。

实现方式

  • :通常使用二叉堆(最小堆或最大堆)来实现优先队列。
  • 有序链表:可以使用有序链表来实现优先队列,但性能较差。

应用

  • 任务调度:操作系统中的任务调度,优先处理高优先级任务。
  • A 搜索算法*:路径搜索算法中,优先处理估计代价较低的节点。
  • 事件模拟:事件驱动的模拟系统中,根据事件的优先级处理事件。
class PriorityQueue {
    constructor() {
        this.items = [];
    }

    enqueue(element, priority) {
        const queueElement = { element, priority };
        let added = false;
        for (let i = 0; i < this.items.length; i++) {
            if (queueElement.priority < this.items[i].priority) {
                this.items.splice(i, 0, queueElement);
                added = true;
                break;
            }
        }
        if (!added) {
            this.items.push(queueElement);
        }
    }

    dequeue() {
        if (this.isEmpty()) {
            return 'Queue is empty';
        }
        return this.items.shift().element;
    }

    peek() {
        if (this.isEmpty()) {
            return 'Queue is empty';
        }
        return this.items[0].element;
    }

    isEmpty() {
        return this.items.length === 0;
    }

    size() {
        return this.items.length;
    }
}

// 示例
const pq = new PriorityQueue();
pq.enqueue('Task 1', 2);
pq.enqueue('Task 2', 1);
console.log(pq.dequeue()); // 输出: Task 2 (优先级最高)
console.log(pq.peek()); // 输出: Task 1

2. 双端队列(Deque, Double-ended Queue)

概念: 双端队列是一种可以在两端进行插入和删除操作的数据结构。相比于单端队列,双端队列提供了更多的灵活性。

操作

  • 从前端插入/删除addFirst, removeFirst
  • 从后端插入/删除addLast, removeLast

应用

  • 缓存机制:双端队列常用于实现双端缓存(如 LRU 缓存)策略。
  • 回溯算法:可以用于回溯算法中的路径跟踪。
class Deque {
    constructor() {
        this.items = [];
    }

    addFirst(element) {
        this.items.unshift(element);
    }

    addLast(element) {
        this.items.push(element);
    }

    removeFirst() {
        if (this.isEmpty()) {
            return 'Deque is empty';
        }
        return this.items.shift();
    }

    removeLast() {
        if (this.isEmpty()) {
            return 'Deque is empty';
        }
        return this.items.pop();
    }

    peekFirst() {
        if (this.isEmpty()) {
            return 'Deque is empty';
        }
        return this.items[0];
    }

    peekLast() {
        if (this.isEmpty()) {
            return 'Deque is empty';
        }
        return this.items[this.items.length - 1];
    }

    isEmpty() {
        return this.items.length === 0;
    }

    size() {
        return this.items.length;
    }
}

// 示例
const deque = new Deque();
deque.addLast(1);
deque.addFirst(2);
console.log(deque.peekFirst()); // 输出: 2
console.log(deque.peekLast()); // 输出: 1
console.log(deque.removeLast()); // 输出: 1
console.log(deque.removeFirst()); // 输出: 2

3. 循环队列(Circular Queue)

概念: 循环队列是一种优化的队列实现,其结构呈环状。即当队列的末尾达到了数组的末尾时,新的元素会从数组的开头位置插入,形成一个环。这种结构有效利用了内存空间。

操作

  • 入队enqueue
  • 出队dequeue
  • 检查是否为空isEmpty
  • 检查是否已满isFull

应用

  • 缓冲区:如计算机网络中的数据包缓冲区。
  • 任务调度:在循环队列中处理任务轮转。
class CircularQueue {
    constructor(capacity) {
        this.queue = new Array(capacity);
        this.front = 0;
        this.rear = 0;
        this.size = 0;
        this.capacity = capacity;
    }

    enqueue(element) {
        if (this.isFull()) {
            return 'Queue is full';
        }
        this.queue[this.rear] = element;
        this.rear = (this.rear + 1) % this.capacity;
        this.size++;
    }

    dequeue() {
        if (this.isEmpty()) {
            return 'Queue is empty';
        }
        const element = this.queue[this.front];
        this.front = (this.front + 1) % this.capacity;
        this.size--;
        return element;
    }

    isEmpty() {
        return this.size === 0;
    }

    isFull() {
        return this.size === this.capacity;
    }

    peek() {
        if (this.isEmpty()) {
            return 'Queue is empty';
        }
        return this.queue[this.front];
    }
}

// 示例
const cQueue = new CircularQueue(3);
cQueue.enqueue(1);
cQueue.enqueue(2);
cQueue.enqueue(3);
console.log(cQueue.enqueue(4)); // 输出: Queue is full
console.log(cQueue.dequeue()); // 输出: 1
cQueue.enqueue(4);
console.log(cQueue.peek()); // 输出: 2

4. 单调队列(Monotonic Queue)

概念: 单调队列是一种特殊的队列,队列中的元素按照某种顺序(递增或递减)排列。常用于解决滑动窗口最小值/最大值问题。

操作

  • 插入元素:维护队列的单调性。
  • 删除元素:在窗口滑动时删除不符合要求的元素。

应用

  • 滑动窗口问题:在给定的窗口中找到最小值或最大值。
class MonotonicQueue {
    constructor() {
        this.deque = [];
    }

    push(value) {
        while (this.deque.length && this.deque[this.deque.length - 1] < value) {
            this.deque.pop();
        }
        this.deque.push(value);
    }

    pop(value) {
        if (this.deque[0] === value) {
            this.deque.shift();
        }
    }

    max() {
        return this.deque[0];
    }
}

// 示例
const mQueue = new MonotonicQueue();
mQueue.push(1);
mQueue.push(3);
mQueue.push(2);
console.log(mQueue.max()); // 输出: 3
mQueue.pop(3);
console.log(mQueue.max()); // 输出: 2

结束

明天定一个小flag :Leetcode刷十道算法,去把现在碰到的数据结构都敲上一遍,特殊链表,特殊队列这些。