Skip to content

栈是一种后入先出(LIFO)的线性逻辑存储结构。只允许在栈顶进行进出操作。

栈基本操作

基本操作包括:入栈(push)/出栈(pop)/获取栈顶元素(peek)。

栈的实现主要有两种:

  1. 数组实现,即顺序栈;
  2. 链表实现,即链式栈。

无论是以数组还是以链表实现,入栈、出栈的时间复杂度都是O(1)。

栈的应用比如函数执行/括号匹配/表达式计算/浏览器前进后退。

设计栈

1. 数组实现栈

ts
// 数组栈
export default class ArrayStack<T> {
  items: T[];

  constructor() {
    this.items = [];
  }

  /**
   * 入栈
   * @param item
   */
  push(item: T) {
    this.items.push(item);
  }

  /**
   * 出栈
   * @returns
   */
  pop() {
    if (this.isEmpty) throw new Error('栈空');
    return this.items.pop();
  }

  /**
   * 获取栈顶元素
   * @returns
   */
  peek() {
    if (this.isEmpty) throw new Error('栈空');
    return this.items[this.size - 1];
  }

  /**
   * 判空
   * @returns
   */
  get isEmpty() {
    return this.size === 0;
  }

  /**
   * 获取栈元素的个数
   * @returns
   */
  get size() {
    return this.items.length;
  }
}

2. 链表实现栈

ts
import LinkNode from '../../链表/设计链表/linkNode';

// 链表栈
export default class LinkStack<T> {
  // 栈的长度
  size: number;

  // 栈顶指针
  top: LinkNode<T> | null;

  constructor() {
    this.size = 0;
    this.top = null;
  }

  /**
   * 入栈
   * @param item
   */
  push(val: T) {
    const node = new LinkNode(val);
    if (this.isEmpty) {
      // 栈空
      this.top = node;
    } else {
      // 栈非空
      node.next = this.top;
      this.top = node;
    }
    this.size += 1;
  }

  /**
   * 出栈
   * @returns
   */
  pop() {
    if (this.isEmpty) {
      // 栈空
      throw new Error('栈空');
    }
    // 栈非空
    const data = this.top!.val; // 栈顶元素值
    this.top = this.top!.next; // 新栈顶
    this.size -= 1;
    return data;
  }

  /**
   * 获取栈顶元素
   * @returns
   */
  peek() {
    if (this.isEmpty) {
      // 栈空
      throw new Error('栈空');
    }
    return this.top!.val;
  }

  /**
   * 判空
   * @returns
   */
  get isEmpty() {
    return this.top === null;
  }
}
ts
// 节点
export default class LinkNode<T> {
  val: T;

  next: LinkNode<T> | null;

  constructor(val: T, next?: LinkNode<T> | null) {
    this.val = val;
    this.next = next ?? null;
  }
}

算法题

1. 包含 min 函数的栈

题目描述:定义栈的数据结构,请在该类型中实现一个能够得到栈的最小元素的 min 函数在该栈中,调用 min、push 及 pop 的时间复杂度都是 O(1)。

分析

数据栈+最小栈法,利用一个最小栈存放当前数据栈的最小元素,保持两个栈的长度相同。从首个元素入数据栈开始,每个元素均入数据栈。同时,判断当前元素与最小栈栈顶元素的大小,将小的元素入最小栈即可。

求解

ts
// 数据栈+最小栈法
class MinStack {
  dataStack: number[];

  minStack: number[];

  constructor() {
    this.dataStack = [];
    this.minStack = [];
  }

  push(x: number): void {
    const dataSize = this.dataStack.length;
    const minSize = this.minStack.length;
    if (dataSize === 0 || this.minStack[minSize - 1] > x) {
      this.minStack.push(x);
    } else {
      this.minStack.push(this.minStack[minSize - 1]);
    }
    this.dataStack.push(x);
  }

  pop(): void {
    const dataSize = this.dataStack.length;
    if (dataSize) {
      this.minStack.pop();
      this.dataStack.pop();
    }
  }

  top(): number {
    const dataSize = this.dataStack.length;
    if (dataSize) {
      return this.dataStack[dataSize - 1];
    }
    return NaN;
  }

  min(): number {
    const minSize = this.dataStack.length;
    if (minSize) {
      return this.minStack[minSize - 1];
    }
    return NaN;
  }
}

2. 栈的压入、弹出序列

题目描述:输入两个整数序列,第一个序列表示栈的压入顺序,请判断第二个序列是否为该栈的弹出顺序。假设压入栈的所有数字均不相等。例如,序列 {1,2,3,4,5} 是某栈的压栈序列,序列 {4,5,3,2,1} 是该压栈序列对应的一个弹出序列,但 {4,3,5,1,2} 就不可能是该压栈序列的弹出序列。

分析

辅助栈模拟法,借用一个辅助栈 stack ,模拟压入/弹出操作的排列。根据是否模拟成功,即可得到结果。首先按照压栈序列的顺序执行辅助栈入栈操作,每次压栈序列中的元素入栈时,若栈不为空且栈顶元素等于弹出序列当前应弹出元素,即对栈进行出栈操作并更新弹出元素,最终判断栈是否为空即可。

求解

ts
// 辅助栈模拟法
export default function validateStackSequences(pushed: number[], popped: number[]): boolean {
  const stack: number[] = [];
  const size = pushed.length;
  // 弹出序列当前应该弹出元素的下标
  let poppedIndex = 0;
  // 出栈和入栈序列不全为空
  for (let i = 0; i < size; i += 1) {
    const element = pushed[i];
    // 将入栈序列元素入栈
    stack.push(element);
    while (stack.length && stack[stack.length - 1] === popped[poppedIndex]) {
      // 栈不为空 且 栈顶元素等于弹出序列当前应弹出元素
      stack.pop();
      // 下一个应该弹出元素的下标
      poppedIndex += 1;
    }
  }
  return stack.length === 0; // 弹出与压入对应说明满足
}

队列

队列是一种先入先出(FIFO)的线性逻辑存储结构。只允许在队首进行出队(即 delete 删除)操作,队尾进行入队(即 insert 插入)操作。

队列基本操作

队列的基本操作包括:入队 (enqueue)/ 出队 (dequeue)/ 获取队头元素(peek)

队列的实现主要有两种:

  1. 数组实现,即顺序队列。
  2. 链表实现,即链式队列。

无论是以数组还是以链表实现,入队、出队的时间复杂度都是 O(1)。

队列的应用比如线程池、资源池、消息队列、异步队列。

设计队列

1.顺序队列

数组实现,使用shift出队时每次都要移动队列元素,效率不高。改进方案是可以队列初始化时就需要规定队列长度,通过判断队尾是否有空间,有就让元素一直入队,直到队尾没有空间位置,然后进行整体进行一次搬移,这样优化了入队的效率,平均时间复杂度还是 O(1)。

ts
// 数组实现顺序队列
export default class ArrayQueue<T> {
  items: T[];

  constructor() {
    this.items = [];
  }

  /**
   * 入队
   * @param item
   */
  push(item: T) {
    this.items.push(item);
  }

  /**
   * 出队
   * @returns
   */
  pop() {
    if (this.isEmpty()) throw new Error('队列空');
    return this.items.shift();
  }

  /**
   * 获取队顶元素
   * @returns
   */
  peek() {
    if (this.isEmpty()) throw new Error('队列空');
    return this.items[0];
  }

  /**
   * 判空
   * @returns
   */
  isEmpty() {
    return this.items.length === 0;
  }

  /**
   * 获取队元素的个数
   * @returns
   */
  getSize() {
    return this.items.length;
  }
}

2.循环队列

数组实现,初始化需指定队列容量,留一个空位,队空条件 head = tail,队满条件 head =( tail + 1) % capacity,队列元素个数 (tail - head + capacity) % capacity。

ts
// 数组循环队列
export default class LoopQueue {
  // 存放元素的数组
  values: (number | undefined)[];

  // 当前元素个数
  count: number;

  // 队的长度
  capacity: number;

  // 队尾
  head: number;

  // 队尾
  tail: number;

  constructor(capacity: number) {
    this.head = 0;
    this.tail = 0;
    this.capacity = capacity;
    this.count = 0;
    this.values = new Array(capacity);
  }

  /**
   * 入队
   * @param item
   */
  enQueue(val: number) {
    if (this.isFull()) {
      throw new Error('队满');
    }
    this.values[this.tail] = val;
    this.tail = (this.tail + 1) % this.capacity;
    this.count += 1;
    return true;
  }

  /**
   * 出队
   * @returns
   */
  deQueue(): number {
    if (this.isEmpty()) {
      throw new Error('队空');
    }
    const value = this.values[this.head] as number;
    this.values[this.head] = undefined;
    this.head = (this.head + 1) % this.capacity;
    this.count -= 1;
    return value;
  }

  /**
   * 获取队头元素
   * @returns
   */
  peek() {
    if (this.isEmpty()) {
      throw new Error('队空');
    }
    const value = this.values[this.head];
    return value;
  }

  /**
   * 判空
   * @returns
   */
  isEmpty() {
    // 或 return this.head === this.tail
    return this.count === 0;
  }

  /**
   * 判满
   * @returns
   */
  isFull() {
    // 或 return this.head === (this.tail + 1) % this.capacity
    return this.count === this.capacity - 1;
  }

  /**
   * 获取队元素的个数
   * @returns
   */
  getSize() {
    return this.count;
  }

  /**
   * 清空队列
   * @returns
   */
  clear() {
    this.head = 0;
    this.tail = 0;
    this.count = 0;
    this.values = new Array(this.capacity);
    return true;
  }
}

3.链式队列

链表实现,链表尾入队,链表头出队。

ts
import LinkNode from '../../链表/设计链表/linkNode';

// 链表顺序队列
export default class LinkQueue<T> {
  // 队的长度
  size: number;

  // 队尾指针
  head: LinkNode<T> | null;

  // 队尾指针
  tail: LinkNode<T> | null;

  constructor() {
    this.size = 0;
    this.head = null;
    this.tail = null;
  }

  /**
   * 入队
   * @param item
   */
  enQueue(val: T) {
    const node = new LinkNode(val);
    if (this.size === 0) {
      this.head = node;
      this.tail = node;
    } else {
      this.tail!.next = node;
      this.tail = this.tail!.next;
    }
    this.size += 1;
  }

  /**
   * 出队
   * @returns
   */
  deQueue() {
    if (this.size === 0) {
      // 队空
      throw new Error('队空');
    } else {
      // 队非空
      const node = this.head;
      this.head = node!.next;
      this.size -= 1;
      return node!.val;
    }
  }

  /**
   * 获取队头元素
   * @returns
   */
  peek() {
    if (this.size === 0) {
      // 队空
      throw new Error('队空');
    } else {
      return this.head!.val;
    }
  }

  /**
   * 判空
   * @returns
   */
  isEmpty() {
    return this.size === 0;
  }

  /**
   * 获取队元素的个数
   * @returns
   */
  getSize() {
    return this.size;
  }
}
ts
// 节点
export default class LinkNode<T> {
  val: T;

  next: LinkNode<T> | null;

  constructor(val: T, next?: LinkNode<T> | null) {
    this.val = val;
    this.next = next ?? null;
  }
}

算法题

1. 两个栈实现队列

题目描述:用两个栈来实现一个队列,完成队列的Push和Pop操作。 队列中的元素为int类型。

分析

栈用数组模拟,队列只允许在队首进行出队(即delete/pop删除 )操作,队尾进行入队(即insert/push插入)操作,因此其中一个栈 stack1用于入队,另一个栈stack2用于出队,出队时判断出队栈 stack2 是否为空:

  1. 空则将入队栈 stack1 中的元素全部转移至出队栈 stack2,然后出队栈 stack2 栈顶元素出队即可
  2. 非空,则出队栈 stack2 栈顶元素直接出队。

求解

ts
// 数组模拟
class CQueue {
  pushStack: number[];

  popStack: number[];

  constructor() {
    this.pushStack = [];
    this.popStack = [];
  }

  appendTail(value: number): void {
    this.pushStack.push(value);
  }

  deleteHead(): number {
    if (this.pushStack.length === 0 && this.popStack.length === 0) {
      // 队列为空
      return -1;
    }
    // 出队的栈为空时,依次将入队的栈中元素先入出队的栈
    if (this.popStack.length === 0) {
      while (this.pushStack.length) {
        this.popStack.push(this.pushStack.pop()!);
      }
    }
    return this.popStack.pop()!;
  }
}