JavaScript 数据结构与算法(二):栈与队列

是限定仅 在表尾进行插入和删除操作 的线性表。 即 先进后出,后进先出

应用软件 中,栈这种后进先出的数据结构的应用是非常普遍的。 比如 ,浏览器的“后退”键;Word 的撤销操作;等等。

理解

我们把 允许插入和删除的一端称为栈顶(top),另一端称为(栈底),不含任何数据元素的栈称为空栈。 栈又称为后进先出(Last In First Out)的线性表,简称 LIFO 结构。

对栈的操作

对栈的两种主要操作是 将一个元素压入栈和将一个元素弹出栈 。入栈使用 push() 方法,出栈使用 pop() 方法。

另一个常用的操作是 预览栈顶的元素peek() 方法只返回栈顶元素,而不删除它。

pop() 方法虽然可以访问栈顶的元素,但是调用该方法后,栈顶元素也从栈中被永久性地删除了。

🐱 Stack 类
function Stack() {
  this.dataStore = [];
  this.top = 0;
  this.push = push;
  this.pop = pop;
  this.peek = peek;
  this.clear = clear;
  this.length = length;
}

function push(element) {
  this.dataStore[this.top++] = element;
}

function peek() {
  return this.dataStore[this.top - 1];
}

function pop() {
  return this.dataStore[--this.top];
}

function clear() {
  this.top = 0;
}

function length() {
  return this.top;
}
🐱 测试 Stack 类的实现
var s = new Stack();
s.push("Censek");
s.push("Doris");
s.push("Anna");
console.log("length: " + s.length());
console.log(s.peek());
var popped = s.pop();
console.log("The popped name is: " + popped);
console.log(s.peek());
s.push("Linda");
console.log(s.peek());
s.clear();
console.log("length: " + s.length());
console.log(s.peek());
s.push("Zander");
console.log(s.peek());

输出结果:

length: 3
Anna
The popped name is: Anna
Doris
Linda
length: 0
undefined
Zander

倒数第二行返回 undefined,这是因为栈被清空后,栈顶就没值了,这时使用 peek() 方法 预览栈顶元素,自然得到 undefined

应用
🍊 数制间的相互转换

可以利用栈将一个数字从一种数制转换成另一种数制。假设想将数字 n 转换为以 b 为基数的数字,实现转换的算法如下:

(1) 最高位为 n % b,将此位压入栈。
(2) 使用 n/b 代替 n。
(3) 重复步骤 1 和 2,直到 n 等于 0,且没有余数。
(4) 持续将栈内元素弹出,直到栈为空,依次将这些元素排列,就得到转换后数字的字符
串形式。

此算法只针对基数为 2~9 的情况。

🌰 例子: 将数字转换为八进制数。

function mulBase(num, base) {
  var s = new Stack();
  do {
    s.push(num % base);
    num = Math.floor(num /= base);
  } while (num > 0);
  var converted = "";
  while (s.length() > 0) {
    converted += s.pop();
  }
  return converted;
}

num = 9;
base = 8;
var newNum = mulBase(num, base);
console.log(num + " converted to base " + base + " is " + newNum);

输出为:

9 converted to base 8 is 11
🍊 判断字符串是否为回文

回文:一个单词、短语或数字,从前往后写和从后往前写都是一样的。

我们将拿到的字符串的每个字符按从左至右的顺序压入栈,然后持续弹出栈中的每个字母得到一个新字符串。比较这两个字符串,如果它们相等,就是一个回文。

function isPalindrome(word) {
  var s = new Stack();
  for (var i = 0; i < word.length; ++i) {
    s.push(word[i]);
  }
  var rword = "";
  while (s.length() > 0) {
    rword += s.pop();
  }
  if (word == rword) {
    return true;
  } else {
    return false;
  }
}
var word = "hello";
if (isPalindrome(word)) {
  console.log(word + " is a palindrome.");
} else {
  console.log(word + " is not a palindrome.");
}
word = "racecar"
if (isPalindrome(word)) {
  console.log(word + " is a palindrome.");
} else {
  console.log(word + " is not a palindrome.");
}

输出结果为:

hello is not a palindrome. 
racecar is a palindrome.
🍊 递归演示

比如,使用栈来模拟计算 5! 的过程,首先将数字从 5 到 1 压入栈,然后使用一个循环,将数字挨个弹出连乘,就得到了正确的答案。

🍊 四则运算表达式求值
  • 将中缀表达式转化为后缀表达式(栈用来进出运算的符号)
  • 将后缀表达式进行运算得出结果(栈用来进出运算的数字)
栈的链式存储结构

简称为 链栈

把栈顶放在单链表的头部,如下图所示。通常对于链栈来说,是不需要头结点的。

对于链栈来说,基本不存在栈满的情况,除非内存已经没有可以使用的空间(计算机操作系统面临死机崩溃的情况)。

队列

队列 是只允许 在一端进行插入操作,而在另一端进行删除操作 的线性表。即 先进先出

操作系统客服系统 中,都应用了队列的数据结构。

理解

队列是一种先进先出(First In First Out)的线性表,简称 FIFO。允许插入的一端称为队尾,允许删除的一端称为队头。

对队列的操作

队列的两种主要操作是:向队列中插入新元素和删除队列中的元素。插入操作也叫做入队,删除操作也叫做出队。入队操作在队尾插入新元素,出队操作删除队头的元素。

队列的另外一项重要操作是 读取队头的元素。这个操作叫做 peek()。该操作返回队头元素,但不把它从队列中删除。

🐱 Queue 类
function Queue() {
  this.dataStore = [];
  this.enqueue = enqueue;
  this.dequeue = dequeue;
  this.front = front;
  this.back = back;
  this.toString = toString;
  this.empty = empty;
}

function enqueue(element) { // 方法向队尾添加一个元素
  this.dataStore.push(element);
}

function dequeue() { // 删除队首的元素
  return this.dataStore.shift();
}

function front() { // 读取队首的元素
  return this.dataStore[0];
}

function back() { // 读取队尾的元素
  return this.dataStore[this.dataStore.length - 1];
}

function toString() { // 显示队列内的所有元素
  var retStr = "";
  for (var i = 0; i < this.dataStore.length; ++i) {
    retStr += this.dataStore[i] + "\n";
  }
  return retStr;
}

function empty() { // 判断队列是否为空
  if (this.dataStore.length == 0) {
    return true;
  } else {
    return false;
  }
}
🐱 测试 Queue 类的实现
var q = new Queue();
q.enqueue("Censek");
q.enqueue("Doris");
q.enqueue("Anna");
console.log(q.toString());
q.dequeue();
console.log(q.toString());
console.log("Front of queue: " + q.front());
console.log("Back of queue: " + q.back());

输出为:

Censek
Doris
Anna

Doris
Anna

Front of queue: Doris
Back of queue: Anna
应用
  • 方块舞的舞伴分配问题
  • 排序
队列的链式存储结构

其实就是线性表的单链表,只不过它只能尾进头出而已,简称为 链队列

队头指针指向链队列的头结点,队尾指针指向终端结点。

空队列时, frontrear 都指向头结点。


总结

栈和队列都可以用 线性表的顺序存储结构 来实现,但都存在着顺序存储的一些 弊端 。但它们各有各的技巧来 解决这个问题

对于 来说,如果是两个相同数据类型的栈,则可以 用数组的两端作栈底的方法来让两个栈共享数据 ,这就可以最大化地利用数组的空间。

见附录。

对于 队列 来说,为了避免数组插入和删除时需要移动数据,于是就引入了 循环队列,使得队头和队尾可以在数组中循环变化。解决了移动数据的时间损耗,使得本来插入和删除是 O(n) 的时间复杂度变成了 O(1)。

见附录。

它们也都可以通过链式存储结构来实现,实现原则与线性表基本相同。

附录

两栈共享空间

如果我们有两个相同类型的栈,我们为它们各自开辟了数组空间,极有可能第一个栈已经满了,再进栈就溢出了,而另一个栈还有很多存储空间空闲。

我们可以用一个数组来存储两个栈。做法如下图所示,数组有两个端点,两个栈有两个栈底,让一个栈的栈底为数组的始端,即下标为 0 处,另一个栈为栈的末端,即下标为数组长度 n-1 处。这样 两个栈如果增加元素,就是两端点像中间延伸

增加的元素是在数组的两端向中间靠拢。top1 和 top2 是栈 1 和栈 2 的栈顶指针。只要它们俩不见面,两个栈就可以一直使用。
top1 + 1 === top2 为栈满。

循环队列
队列顺序存储的不足

与栈不同的是,队列元素的出列是在队头,即下标为 0 的位置,也就意味着,队列中的所有元素都得向前移动,以保证队列的队头不为空,此时时间复杂度为 O(n)。

循环队列定义

队列的头尾相接的顺序存储结构称为 循环队列

通用的计算队列长度公式:
(rear - front + QueueSize) % QueueSize



🔗:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值