栈
栈
是限定仅 在表尾进行插入和删除操作 的线性表。 即 先进后出,后进先出
。
在 应用软件 中,栈这种后进先出的数据结构的应用是非常普遍的。 比如 ,浏览器的“后退”键;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
应用
- 方块舞的舞伴分配问题
- 排序
队列的链式存储结构
其实就是线性表的单链表,只不过它只能尾进头出而已,简称为 链队列
。
队头指针指向链队列的头结点,队尾指针指向终端结点。
空队列时, front
和 rear
都指向头结点。
总结
栈和队列都可以用 线性表的顺序存储结构 来实现,但都存在着顺序存储的一些 弊端 。但它们各有各的技巧来 解决这个问题 。
对于 栈 来说,如果是两个相同数据类型的栈,则可以 用数组的两端作栈底的方法来让两个栈共享数据 ,这就可以最大化地利用数组的空间。
见附录。
对于 队列 来说,为了避免数组插入和删除时需要移动数据,于是就引入了 循环队列,使得队头和队尾可以在数组中循环变化。解决了移动数据的时间损耗,使得本来插入和删除是 O(n) 的时间复杂度变成了 O(1)。
见附录。
它们也都可以通过链式存储结构来实现,实现原则与线性表基本相同。
附录
两栈共享空间
如果我们有两个相同类型的栈,我们为它们各自开辟了数组空间,极有可能第一个栈已经满了,再进栈就溢出了,而另一个栈还有很多存储空间空闲。
我们可以用一个数组来存储两个栈。做法如下图所示,数组有两个端点,两个栈有两个栈底,让一个栈的栈底为数组的始端,即下标为 0
处,另一个栈为栈的末端,即下标为数组长度 n-1
处。这样 两个栈如果增加元素,就是两端点像中间延伸。
增加的元素是在数组的两端向中间靠拢。top1 和 top2 是栈 1 和栈 2 的栈顶指针。只要它们俩不见面,两个栈就可以一直使用。
top1 + 1 === top2
为栈满。
循环队列
队列顺序存储的不足
与栈不同的是,队列元素的出列是在队头,即下标为 0 的位置,也就意味着,队列中的所有元素都得向前移动,以保证队列的队头不为空,此时时间复杂度为 O(n)。
循环队列定义
队列的头尾相接的顺序存储结构称为 循环队列
。
通用的计算队列长度公式:
(rear - front + QueueSize) % QueueSize
🔗: