1. 前言
本篇是「数据结构与算法系列」的第3篇,主题是栈与队列,下面进入正题。
2. 栈
栈是一种特殊的线性表,只允许在一端进行操作(这一端被称为栈顶),如图所示:
其中,
33是栈顶元素,11是栈底元素(不过我们一般不讨论栈底元素)
2.1 常见操作
- 入栈:往栈中
添加元素的操作,叫做入栈(push),如图所示:
- 出栈:从栈中
移除元素的操作,叫做出栈(pop),如图所示:
由于只能移除栈顶元素,所以
出栈也叫做弹出栈顶元素
栈的特点:后进先出(LIFO,即Last In First Out)
2.2 栈结构的实现
2.2.1 接口设计
如果要设计一个栈结构,通常要实现以下基本接口:
@protocol StackProtocol <NSObject>
@required
/// 元素的数量
- (NSUInteger)size;
/// 是否为空
- (BOOL)isEmpty;
/// 入栈
- (void)push:(id)element;
/// 出栈
- (id)pop;
/// 获取栈顶元素
- (id)top;
/// 清空
- (void)clear;
@end
2.2.2 具体实现
创建Stack类,代码如下:
Stack.h:Stack类遵循StackProtocol协议
@interface Stack : NSObject <StackProtocol>
@end
Stack.m:由于入栈、出栈改变的都只是栈顶元素,所以这里选择用动态数组(NSMutableArray)实现,时间复杂度为O(1)
@interface Stack ()
@property (nonatomic, strong) NSMutableArray *list;
@end
@implementation Stack
- (instancetype)init
{
if (self = [super init]) {
self.list = [[NSMutableArray alloc] init];
}
return self;
}
#pragma mark - StackProtocol
/// 元素的数量
- (NSUInteger)size
{
return _list.count;
}
/// 是否为空
- (BOOL)isEmpty
{
return _list.count == 0;
}
/// 入栈
- (void)push:(id)element
{
if (element) {
[_list addObject:element];
}
}
/// 出栈
- (id)pop
{
if (_list.count > 0) {
id lastObj = _list.lastObject;
[_list removeLastObject];
return lastObj;
}
return nil;
}
/// 获取栈顶元素
- (id)top
{
if (_list.count > 0) {
return _list.lastObject;
}
return nil;
}
/// 清空
- (void)clear
{
[_list removeAllObjects];
}
@end
2.2.3 代码测试
在main.m中添加测试代码,如下:
#import <Foundation/Foundation.h>
#import "Stack.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
Stack *stack = [[Stack alloc] init];
[stack push:@11];
[stack push:@22];
[stack push:@33];
[stack push:@44];
while (![stack isEmpty]) {
NSLog(@"pop item: %lld", ((NSNumber *)[stack pop]).longLongValue);
}
}
return 0;
}
运行后的输出为:
显然测试通过!
2.3 栈的应用
栈的应用场景对于我们并不陌生,如:
- 软件的撤销(Undo)、回复(Redo)功能
- 浏览器的前进、后退功能
2.4 练习:有效的括号
接下来用个简单的 leetcode算法题:20-有效的括号 练习一下
2.4.1 问题描述(摘自leetcode)
给定一个只包括 '(',')','{','}','[',']' 的字符串 s ,判断字符串是否有效。
- 有效字符串需满足:
- 左括号必须用相同类型的右括号闭合。
- 左括号必须以正确的顺序闭合。
- 示例:
输入:s = "()[]{}"
输出:true
输入:s = "([)]"
输出:false
输入:s = "{[]}"
输出:true
输入:s = "{[])}"
输出:false
2.4.2 解题思路
考虑到用栈解题,就必须想好入栈、出栈的操作,思路如下:
- 对字符串逐个字符遍历;
- 如果是左字符(
(、[、{),则入栈; - 如果是右字符(
)、]、}),判断栈是否为空:- 如果栈是空的,说明括号无效,直接
return false - 如果栈不为空,则弹出栈顶字符,与右字符匹配
- 若结果匹配,继续扫描下一个字符
- 若结果不匹配,说明括号无效,直接
return false
- 如果栈是空的,说明括号无效,直接
- 所有字符扫描完毕后,如果:
- 栈为空,说明括号有效,
return true; - 栈不为空,说明括号无效,
return false;
- 栈为空,说明括号有效,
2.4.3 具体实现
具体实现的代码如下:
/**
有效的括号:leetcode_20(https://leetcode-cn.com/problems/valid-parentheses/)
*/
+ (BOOL)isValidBrackets:(NSString *)brackets
{
if (!brackets || !brackets.length) {
return NO;
}
Stack *stack = [[Stack alloc] init];
NSDictionary<NSString *, NSString *> *dict = @{
@"(" : @")",
@"[" : @"]",
@"{" : @"}",
};
NSArray *allKeys = dict.allKeys;
NSUInteger length = brackets.length;
NSRange range;
for (int i = 0; i < length; i += range.length) {
range = [brackets rangeOfComposedCharacterSequenceAtIndex:i];
NSString *sub = [brackets substringWithRange:range];
if ([allKeys containsObject:sub]) {// 左括号
[stack push:sub];
} else {// 右括号
if (stack.isEmpty || ![sub isEqualToString:dict[stack.pop]]) {
return NO;
}
}
}
return stack.isEmpty;
}
2.4.4 代码测试
测试代码如下:
#import "StackDemo.h"
int main(int argc, const char * argv[]) {
@autoreleasepool {
NSArray<NSString *> *expArray = @[@"()[]{}", @"([)]", @"{[]}", @"{[])}"];
for (NSString *exp in expArray) {
NSLog(@"%@ isValid: %@", exp, [StackDemo isValidBrackets:exp] ? @"true" : @"false");
}
}
return 0;
}
结果如下图:
测试通过!
3. 队列
队列是一种特殊的线性表,可以在两端进行操作,如图所示:
3.1 常见操作
队列的双端分别是队尾(rear)、队头(front),其常见操作如下:
- 入队:往队列中
添加元素的操作,叫做入队(即enqueue),如图所示:
入队的那一端叫做
队尾(rear)
- 出队:从队列中
移除元素的操作,叫做出队(即dequeue),如图所示:
出队的那一端叫做
队头(front)
队列的特点:先进先出(FIFO,即First In First Out)
3.2 队列结构的实现
3.2.1 接口设计
队列的通用接口与栈类似,如:
@protocol QueueProtocol <NSObject>
@required
/// 元素的数量
- (NSUInteger)size;
/// 是否为空
- (BOOL)isEmpty;
/// 入队
- (void)enqueue:(id)element;
/// 出队
- (id)dequeue;
/// 获取队列的头元素
- (id)front;
/// 清空
- (void)clear;
@end
3.2.2 具体实现
创建Queue类,具体的代码实现如下:
Queue.h:Queue类遵循QueueProtocol协议
@interface Queue : NSObject <QueueProtocol>
@end
Queue.m:由于队列是双端操作(头尾),所以优先使用双向链表(DoublyLinkedList),能够让队列的操作的时间复杂度达到O(1)级别。对
双向链表感兴趣的,请移步 数据结构与算法:链表,或者访问 博主的github
@interface Queue ()
@property (nonatomic, strong) DoublyLinkedList *list;
@end
@implementation Queue
- (instancetype)init
{
if (self = [super init]) {
self.list = [[DoublyLinkedList alloc] init];
}
return self;
}
#pragma mark - QueueProtocol
/// 元素的数量
- (NSUInteger)size
{
return [_list size];
}
/// 是否为空
- (BOOL)isEmpty
{
return [_list isEmpty];
}
/// 入队
- (void)enqueue:(id)element
{
[_list add:element];
}
/// 出队
- (id)dequeue
{
return [_list remove:0];
}
/// 获取队列的头元素
- (id)front
{
return [_list get:0];
}
/// 清空
- (void)clear
{
[_list clear];
}
@end
3.2.3 代码测试
在main.m中添加测试代码,如下:
// main.m
int main(int argc, const char * argv[]) {
@autoreleasepool {
[QueueDemo testQueue];
}
return 0;
}
// QueueDemo.m
+ (void)testQueue
{
Queue *queue = [[Queue alloc] init];
[queue enqueue:@11];
[queue enqueue:@22];
[queue enqueue:@33];
[queue enqueue:@44];
while (![queue isEmpty]) {
NSLog(@"dequeue item: %@", [queue dequeue]);
}
}
运行后的输出为:
显然测试通过!
3.3 队列的应用
队列在日常生活中的应用非常广泛,凡是涉及到 排队 的,基本就是队列了,如:
- 排队打疫苗
- 在车站排队买票等
3.4 双端队列
双端队列:是能在头尾两端进行入队、出队的队列。其英文是deque(double ended queue的缩写)。
双端队列的实现,相对于普通队列,只需要新增以下几个接口即可:
@protocol DequeProtocol <QueueProtocol>
@required
/// 从队尾入队
- (void)enqueueRear:(id)element;
/// 从队头入队
- (void)enqueueFront:(id)element;
/// 从队尾出队
- (id)dequeueRear;
/// 从队头出队
- (id)dequeueFront;
/// 获取队尾元素
- (id)rear;
这几个接口的实现如下:
/// 从队头出队
- (id)dequeueFront
{
return [_list remove:0];
}
/// 从队尾出队
- (id)dequeueRear
{
return [_list remove:(_list.size - 1)];
}
/// 从队头入队
- (void)enqueueFront:(id)element
{
[_list add:element atIndex:0];
}
/// 从队尾入队
- (void)enqueueRear:(id)element
{
[_list add:element];
}
/// 获取队尾元素
- (id)rear
{
return [_list get:(_list.size - 1)];
}
3.4.1 测试队列
双端队列的测试代码如下:
/// 测试双端队列
+ (void)testDeque
{
Deque *queue = [[Deque alloc] init];
[queue enqueueFront:@11];
[queue enqueueFront:@22];
[queue enqueueFront:@33]; // 尾[11, 22, 33]头
[queue enqueueRear:@44];
[queue enqueueRear:@55];
[queue enqueueRear:@66]; // 尾[66, 55, 44, 11, 22, 33]头
[queue dequeueFront]; // 尾[66, 55, 44, 11, 22]头
[queue dequeueRear]; // 尾[55, 44, 11, 22]头
while (![queue isEmpty]) {
NSLog(@"dequeue item: %@", [queue dequeueFront]);
}
}
以及测试结果:
显然测试通过!
4 总结
来到这里,栈与队列的相关内容就介绍完了。现在总结一下:
栈和队列都是特殊的线性表(底层可以由数组实现,也可以由链表实现)栈的特性是后进先出(LIFO),队列的特性是先进先出(FIFO)
由于这两种数据结构相似又各有不同,所以博主才将它们放在同一篇幅。按照惯例,接下来就以一二算法题作为练习吧。
4.1 练习1:用队列实现栈
用队列实现栈是leetcode的第225题,具体题目要求是:
- 请你仅使用两个队列实现一个栈,并支持普通栈的全部四种操作:入栈(push)、出栈(pop)、获取栈顶元素(top)、是否为空(isEmpty)
4.1.1 实现思路
为了满足栈的特性(LIFO,即最后入栈的元素最先出栈),在使用队列实现栈时,最后入栈的元素必然是队头的元素。
由于题目要求用两个队列,我们先对它们进行编号,即queue1、queue2,其中queue1作为主队列,用于存储入栈的元素,queue2作为入栈操作时的辅队列,每一次的栈操作,都要确保辅队列是个空队列(元素数量为0)。具体实现方案如下:
- 入栈时(假如入栈元素为a):
- 把元素a入队到
queue2(此时queue2中只有一个元素a) - 把
queue1的所有元素依次出队,并入队到queue2 - 交换
queue1、queue2
- 把元素a入队到
上述操作结束后,
queue2依然是个空队列,queue1的队头、队尾分别对应栈顶、栈底
- 出栈时:直接调用
queue1的出队方法即可
4.1.2 具体实现
具体代码如下:
@implementation StackByQueue {
Queue *_queue1;
Queue *_queue2;
}
- (instancetype)init
{
self = [super init];
if (self) {
_queue1 = [[Queue alloc] init];
_queue2 = [[Queue alloc] init];
}
return self;
}
#pragma mark - StackProtocol
- (void)clear
{
[_queue1 clear];
}
- (BOOL)isEmpty
{
return [_queue1 isEmpty];
}
/// 出栈
- (id)pop
{
return [_queue1 dequeue];
}
/// 入栈
- (void)push:(id)element
{
[_queue2 enqueue:element];
while (![_queue1 isEmpty]) {
id element = [_queue1 dequeue];
[_queue2 enqueue:element];
}
Queue *temp = _queue1;
_queue1 = _queue2;
_queue2 = temp;
}
- (NSUInteger)size
{
return [_queue1 size];
}
/// 获取栈顶元素
- (id)top
{
return [_queue1 front];
}
@end
4.1.3 代码测试
添加测试代码如下:
+ (void)testStackByQueue
{
StackByQueue *stack = [[StackByQueue alloc] init];
[stack push:@11];
[stack push:@22];
[stack push:@33];
[stack push:@44];
while (![stack isEmpty]) {
NSLog(@"pop item: %lld", ((NSNumber *)[stack pop]).longLongValue);
}
}
测试结果如下:
显然测试通过!
4.1.4 使用一个队列
如果是使用一个队列呢?
无论是使用一个队列还是两个队列实现栈,其核心思想就是,确保栈顶到栈底的元素顺序,与主队列(存储数据的队列)的队头到对尾的元素顺序一致。如此一来,出栈即出队。因此,关键是入栈操作!
当使用一个队列(标记为queue)时,其入栈操作如下(假如入栈元素为a):
- 先记录入栈前的元素数量,记为n(n >= 0);
- a入队
- 再把
queue的前n个元素依次「出队+入队」
入栈实现如下:
/// 入栈
- (void)push:(id)element
{
NSUInteger preSize = [_queue size];
[_queue enqueue:element];
for (NSUInteger i = 0; i < preSize; i++) {
id element = [_queue dequeue];
[_queue enqueue:element];
}
}
其它方法实现在此就不做赘述,代码都已上传到
github(文末附链接)~
4.2 练习2:用栈实现队列
用队列实现栈是leetcode的第232题,具体题目要求是:
- 请你仅使用两个栈实现一个队列,并支持普通队列的全部四种操作:入队(enqueue)、出队(dequeue)、获取队头元素(front)、是否为空(isEmpty)
4.2.1 实现思路
由于栈是后进先出LIFO,一次倾倒就足以保证正序(队列的元素顺序),因此这里用inStack、outStack为两个栈命名,主要思路如下:
- 入队时(假如是元素a):将元素a入栈到
inStack - 出队时,遵循:
- 如果
outStack为空,把inStack中的所有元素逐一弹出,push到outStack,outStack弹出栈顶元素 - 如果
outStack不为空,outStack弹出栈顶元素
- 如果
4.2.2 具体实现
用栈实现队列的具体代码如下:
@implementation QueueByStack
{
Stack *_inStack;
Stack *_outStack;
}
- (instancetype)init
{
self = [super init];
if (self) {
_inStack = [[Stack alloc] init];
_outStack = [[Stack alloc] init];
}
return self;
}
#pragma mark - QueueProtocol
/// 清空元素
- (void)clear
{
[_inStack clear];
[_outStack clear];
}
/// 出队
- (id)dequeue
{
[self checkOutStack];
return [_outStack pop];
}
/// 入队
- (void)enqueue:(id)element
{
[_inStack push:element];
}
/// 获取队头
- (id)front
{
[self checkOutStack];
return [_outStack top];
}
/// 是否为空
- (BOOL)isEmpty
{
return [_inStack isEmpty] && [_outStack isEmpty];
}
- (NSUInteger)size
{
return [_inStack size] + [_outStack size];
}
#pragma mark - Private
/// 如果outStack为空,把inStack中的数据倾倒到outStack中
- (void)checkOutStack
{
if ([_outStack isEmpty]) {
while (![_inStack isEmpty]) {
[_outStack push:[_inStack pop]];
}
}
}
4.2.3 代码测试
添加以下测试代码:
+ (void)testQueueByStack
{
QueueByStack *queue = [[QueueByStack alloc] init];
[queue enqueue:@11];
[queue enqueue:@22];
[queue enqueue:@33];
[queue enqueue:@44];
while (![queue isEmpty]) {
NSLog(@"dequeue item: %@", [queue dequeue]);
}
}
测试结果如下:
显然测试通过!