数据结构与算法学习之双端队列

200 阅读7分钟

双端队列的定义

双端队列是一种允许我们同时从两端进行入队和出队操作的特殊队列。双端队列同时遵守了 先进先出后进先出 原则,因此可以说它是把队列和栈相结合的一种数据结构。

在现实生活中,电影院、餐厅中排队的队伍就是双端队列最好的例子。例如,一个刚买了票的人如果只是还需要再问一些简单的信息,就可以直接回到队伍的头部。另外,队伍末尾的人如果赶时间,他可以直接离开队伍。

在计算机科学中,双端队列的一个常见应用是存储一系列的撤销操作。每当用户在软件中进行了一个操作,该操作会被存在一个双端队列中(就像在一个栈里)。当用户点击撤销按钮时,该操作会被从双端队列中弹出,表示它被从后面移除了。在进行了预先定义的一定数量的操作后,最先进行的操作会被从双端队列的前面移除。

基于Object对象的双端队列

与使用 Object 对象实现队列一样,我们使用 ES6 的类来创建一个基于 Object 对象的双端队列

class Deque {
    constructor() {
    this.count = 0;
    this.lowestCount = 0;
    this.items = {};
  }
}

接下来,我们为双端队列声明一些可用的方法

  • addFront(element) 在双端队列的前端添加新的元素
  • addBack(element) 在双端队列的后端添加新的元素
  • removeFront() 从双端队列的前端移除第一个元素
  • removeBack() 从双端队列的后移除除第一个元素
  • peekFront() 返回双端队列 前端的第一个元素(注意:只是查看,不是删除)
  • peekBack() 返回双端队列 后端的第一个元素(注意:只是查看,不是删除)
  • isEmpty() 判断双端队列是否为空
  • clear() 清空双端队列中的元素
  • size() 返回双端队列中的元素个数
  • toString() 输出双端队列的字符形式

下面,我们逐一实现这些方法:

addFront 在双端队列前端添加元素

addFront 方法将一个元素添加到双端队列的前端

addFront(element) {
    if (this.isEmpty()) {
    this.addBack(element);
  } else if (this.lowestCount > 0) {
    this.lowestCount--;
    this.items[this.lowestCount] = element;
  } else {
    for (let i = this.count; i> 0; i--) {
        this.items[i] = this.items[i - 1];
    }
    this.count++;
    this.items[0] = element;
  }
}

要将一个元素添加到双端队列的前端,会存在三种场景:

第一种场景:双端队列是空的。在这种情况下,我们调用 addBack 方法将元素添加大双端队列的后端,也就是将元素添加到了双端队列的前端。

第二种场景:一个元素已经被从双端队列的前端删除。在这种情况下,lowestCount 的值会大于等于 1,因此,我们只需要将 lowestCount 的值减 1 并将新元素的值放在这个键 的位置上即可。如下所示 的 Deque 类的内部值:

items = {
  1: 8,
  2: 9
}
count = 3;
lowestCount = 1;

如果我们想将元素 7 添加到双端队列的前端,那么将lowestCount的值减 1(新的值是 0),7 会成为键0的值

第三种场景:lowestCount 为 0的情况。在这种情况下,为了便于演示,我们把本场景看作使用数组。要在第一位添加一个新元素,我们需要将所有元素后移一位来空出第一个位置。我们从最后一位开始迭代所有的值,并为元素赋上索引值减 1 位置的值。在所有的元素都移动完后,第一位将是空闲状态,这样就可以用需要添加的新元素来覆盖它了。

addBack 在双端队列后端添加元素

addBack 方法将一个元素添加到双端队列的后端

addBack(element) {
    this.items[this.count] = element;
  this.count++
}

removeFront 移除双端队列前端的第一个元素

removeFront 方法会从双端队列的前端移除第一个元素

removeFront() {
    if (this.isEmpty()) {
    return undefined;
  }

  const result = this.items[this.lowestCount];
  delete this.items[this.lowestCount];
  this.lowestCount++;
  return result;
}

removeBack 移除双端队列后端的第一个元素

removeBack 方法会会从双端队列的后端移除第一个元素

removeBack() {
    if (this.isEmpty()) {
    return undefined;
  }

  this.count--;
  const result = this.items[this.count];
  delete this.items[this.count];
  return result
}

peekFront 返回双端队列前端的第一个元素

peekFront 方法返回双端队列前端的第一个元素,注意,只是查看元素,不是移除元素

peekFront() {
    if (this.isEmpty()) {
    return undefined;
  }
  return this.items[this.lowestCount]
}

peekBack 返回双端队列后端的第一个元素

peekBack 方法返回双端队列后端的第一个元素

peekBack() {
    if (this.isEmpty()) {
    return undefined;
  }
  return this.items[this.count - 1]
}

isEmpty 判断双端队列是否为空

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

clear 清空双端队列中的元素

clear() {
    this.items = {};
  this.count = 0;
  this.lowestCount = 0;
}

size 返回双端队列中的元素个数

size() {
    return this.count - this.lowestCount
}

toString 输出双端队列的字符形式

toString() {
    if (this.isEmpty()) {
    return '';
  }
  let objString = `${this.items[this.lowestCount]}`;
  for (let i = this.lowestCount + 1, i < this.count; i++) {
    objString = `${objString}, ${this.items[i]}`l
  }
  return objString
}

完整代码

class Deque {
  constructor() {
    this.count = 0;
    this.lowestCount = 0;
    this.items = {};
  }

  // 在双端队列前端添加元素
  addFront(element) {
    if (this.isEmpty()) {
      this.addBack(element);
    } else if (this.lowestCount > 0) {
      this.lowestCount--;
      this.items[this.lowestCount] = element;
    } else {
      for (let i = this.count; i > 0; i--) {
        this.items[i] = this.items[i - 1];
      }
      this.count++;
      this.items[0] = element;
    }
  }

    // 在双端队列后端添加元素
  addBack(element) {
    this.items[this.count] = element;
    this.count++;
  }

    // 在双端队列前端移除元素
  removeFront() {
    if (this.isEmpty()) {
      return undefined;
    }
    const result = this.items[this.lowestCount];
    delete this.items[this.lowestCount];
    this.lowestCount++;
    return result;
  }

  // 在双端队列后端移除元素
  removeBack() {
    if (this.isEmpty()) {
      return undefined;
    }
    this.count--;
    const result = this.items[this.count];
    delete this.items[this.count];
    return result;
  }

  // 查看双端队列的前端的第一个元素
  peekFront() {
    if (this.isEmpty()) {
      return undefined;
    }
    return this.items[this.lowestCount];
  }

  // 查看双端队列的后端的第一个元素
  peekBack() {
    if (this.isEmpty()) {
      return undefined;
    }
    return this.items[this.count - 1];
  }

  // 判断双端队列是否为空
  isEmpty() {
    return this.size() === 0;
  }

  // 清空双端队列
  clear() {
    this.items = {};
    this.count = 0;
    this.lowestCount = 0;
  }

  // 返回双端队列的长度
  size() {
    return this.count - this.lowestCount;
  }

  // 将双端队列转换成字符串
  toString() {
    if (this.isEmpty()) {
      return '';
    }
    let objString = `${this.items[this.lowestCount]}`;
    for (let i = this.lowestCount + 1; i < this.count; i++) {
      objString = `${objString},${this.items[i]}`;
    }
    return objString;
  }
}

基于数组的双端队列

同样,我们可以使用的 ES6 的类来创建一个基于数组的双端队列:

class Deque {
    constructor() {
    this.items = []
  }
}

同样,我们为双端队列声明一些可用的方法:

  • addFront(element) 在双端队列的前端添加新的元素
  • addBack(element) 在双端队列的后端添加新的元素
  • removeFront() 从双端队列的前端移除第一个元素
  • removeBack() 从双端队列的后移除除第一个元素
  • peekFront() 返回双端队列 前端的第一个元素(注意:只是查看,不是删除)
  • peekBack() 返回双端队列 后端的第一个元素(注意:只是查看,不是删除)
  • isEmpty() 判断双端队列是否为空
  • clear() 清空双端队列中的元素
  • size() 返回双端队列中的元素个数
  • toString() 输出双端队列的字符形式

下面,我们逐一实现这些方法:

addFront 在双端队列前端添加元素

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

addBack 在双端队列后端添加元素

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

removeFront 移除双端队列前端的第一个元素

removeFront() {
    if (this.isEmpty()) {
    return undefined;
  }
  return this.items.shift()
}

removeBack() 移除双端队列后端的第一个元素

removeBack() {
    if (this.isEmpty()) {
    return undefined;
  }
  return this.items.pop();
}

peekFront() 返回双端队列前端的第一个元素

peekFront() {
    if (this.isEmpty()) {
    return undefined;
  }
  return this.items[0];
}

peekBack() 返回双端队列后端的第一个元素

peekBack() {
    if (this.isEmpty()) {
    return undefined;
  }
  return this.items[this.items.length - 1];
}

isEmpty() 判断双端队列是否为空

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

clear() 清空双端队列中的元素

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

size() 返回双端队列中的元素个数

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

toString() 输出双端队列的字符形式

toString() {
    if (this.isEmpty()) {
    return '';
  }
  return this.items.toString();
}

完整代码

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

  addFront(element) {
    this.items.unshift(element
  }

  addBack(element) {
    this.items.push(element)
  }

  removeFront() {
    if (this.isEmpty()) {
      return undefined;
    }
    return this.items.shift();

  }

  removeBack() {
    if (this.isEmpty()) {
      return undefined;
    }
    return this.items.pop();
  }

  peekFront() {
    if (this.isEmpty()) {
      return undefined;
    }
    return this.items[0];
  }

  peekBack() {
    if (this.isEmpty()) {
      return undefined;
    }
    return this.items[this.items.length - 1];
  }

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

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

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

  toString() {
    if (this.isEmpty()) {
      return '';
    }
    return this.items.toString();
  }
}

双端队列的应用

回文检查器

回文是正反都能读通的单词、词组、数或句子,例如数字:11、131,单词:level、reviver。

有不同的算法可以检查一个词组或字符串是否为回文,最简单的方式时将字符串反向排列并检查它和原字符串是否相同,如果两者相同,那么它就是一个回文。我们也可以用栈来实现,但是利用数据结构来解决这个问题的最简单方法是使用双端队列。

思路分析:

  1. 首先检查传入的字符串是否合法,如果不合法,返回 false;
  2. 由于可能接收到包含大小写字母的字符串,因此需要将所有字母转换为小写,并同时移除所有的空格;
  3. 然后将字符串中的所有字符都放入双端队列中;
  4. 同时从双端队列的两端移除一个元素,比较两个字符是否相同,直至双端队列中只剩下一个元素,如果此时比较的结果是两个字符相同,则这个字符串是一个回文,否则这个字符串不是一个回文

算法实现:

const palindromeChecker = (str) => {
  // 检查传入的字符串是否合法
    if (str === undefined || str === null || (str !== null && str.length === 0)) {
    return false;
  }

  const deque = new Deque();
  // 将字符串转换成小写,移除空格
  let lowerString = str.toLocaleLowerCase().split(' ').join('');
  let isEqual = true;
  let firstChar, lastChar;

  // 将字符串放入双端队列中
  for (let i = 0; i < lowerString.length; i++) {
    deque.addBack(lowerString.charAt(i));
  }

  // 同时在两端对字符串进行出队操作,比较两个字符是否相同
  while (deque.size() > 1 && isEqual) {
    firstChar = deque.removeFront();
    lastChar = deque.removeBack();
    if (firstChar !== lastChar) {
        isEqual = false;
    }
  }

  return isEqual;
}