栈
栈遵循先入后出的原则,因此我们只能在栈顶添加或删除元素。然而,数组和链表都可以在任意位置添加和删除元素,因此栈可以视为一种受限制的数组或链表。换句话说,我们可以"屏蔽"数组或链表的部分无关操作,使其对外表现的逻辑符合栈的特性。
所以栈只能改变栈顶的数据
具体实现一个栈(链表实现):
kotlin
/* 基于链表实现的栈 */
class LinkedListStack {
#stackPeek; // 将头节点作为栈顶
#stkSize = 0; // 栈的长度
constructor() {
this.#stackPeek = null;
}
/* 获取栈的长度 */
get size() {
return this.#stkSize;
}
/* 判断栈是否为空 */
isEmpty() {
return this.size === 0;
}
/* 入栈 */
push(num) {
const node = new ListNode(num);
node.next = this.#stackPeek;
this.#stackPeek = node;
this.#stkSize++;
}
/* 出栈 */
pop() {
const num = this.peek();
this.#stackPeek = this.#stackPeek.next;
this.#stkSize--;
return num;
}
/* 访问栈顶元素 */
peek() {
if (!this.#stackPeek) throw new Error('栈为空');
return this.#stackPeek.val;
}
/* 将链表转化为 Array 并返回 */
toArray() {
let node = this.#stackPeek;
const res = new Array(this.size);
for (let i = res.length - 1; i >= 0; i--) {
res[i] = node.val;
node = node.next;
}
return res;
}
}
当然栈也可以使用数组实现,具体的优劣如下:
1.数据的平均效率是更高的因为一般数组是一段连续的内存
2.但是数组是需要动态扩容的,所以在面临动态扩容的时候下一次操作的时间复杂度会变成O(n)
浏览器撤销与栈
浏览器撤销功能的数据结构就是栈,一般而言有两个栈一个为现有使用的一个为被撤回的,当一个撤回操作进行的时候,其栈顶元素被弹出到另外一个栈顶,此时如果用户实行反撤回,则需要从另外一个栈顶把那个元素弹出来然后回到现有使用的栈
队列
队列有区别与栈,栈是先进后出而队列是先进先出,所以实际上队列我们需要关注的不止是头而且也要关注尾。
这里附一个使用数组的队列(因为有一些地方可以优化)
kotlin
/* 基于环形数组实现的队列 */
class ArrayQueue {
#nums; // 用于存储队列元素的数组
#front = 0; // 队首指针,指向队首元素
#queSize = 0; // 队列长度
constructor(capacity) {
this.#nums = new Array(capacity);
}
/* 获取队列的容量 */
get capacity() {
return this.#nums.length;
}
/* 获取队列的长度 */
get size() {
return this.#queSize;
}
/* 判断队列是否为空 */
isEmpty() {
return this.#queSize === 0;
}
/* 入队 */
push(num) {
if (this.size === this.capacity) {
console.log('队列已满');
return;
}
// 计算队尾指针,指向队尾索引 + 1
// 通过取余操作实现 rear 越过数组尾部后回到头部
const rear = (this.#front + this.size) % this.capacity;
// 将 num 添加至队尾
this.#nums[rear] = num;
this.#queSize++;
}
/* 出队 */
pop() {
const num = this.peek();
// 队首指针向后移动一位,若越过尾部,则返回到数组头部
this.#front = (this.#front + 1) % this.capacity;
this.#queSize--;
return num;
}
/* 访问队首元素 */
peek() {
if (this.isEmpty()) throw new Error('队列为空');
return this.#nums[this.#front];
}
/* 返回 Array */
toArray() {
// 仅转换有效长度范围内的列表元素
const arr = new Array(this.size);
for (let i = 0, j = this.#front; i < this.size; i++, j++) {
arr[i] = this.#nums[j % this.capacity];
}
return arr;
}
}
讲讲具体优化在哪,优化在于一般而言删除数组的头元素的时间复杂度为o(n)会比较麻烦,所以这里使用了两个变量来控制位置把删除带来的影响给修正了,也就是说在这种情况下不存在真正的删除,只是通过环形数组,在即将发生越界的情况的时候,把尾指针/头指针循环回去了,但是依旧遵从先进先出这一核心思想
双向队列
双向队列是队列的加强版在原有的基础上增加了队首加入和队尾弹出的功能