数据结构与算法:栈与队列

881 阅读10分钟

1. 前言

本篇是「数据结构与算法系列」的第3篇,主题是栈与队列,下面进入正题。

2. 栈

是一种特殊的线性表,只允许在一端进行操作(这一端被称为栈顶),如图所示:

image.png

其中,33是栈顶元素,11是栈底元素(不过我们一般不讨论栈底元素)

2.1 常见操作

  • 入栈:往栈中添加元素的操作,叫做入栈(push),如图所示:

image.png

  • 出栈:从栈中移除元素的操作,叫做出栈(pop),如图所示:

image.png

由于只能移除栈顶元素,所以出栈也叫做弹出栈顶元素

栈的特点:后进先出(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.hStack类遵循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;
}

运行后的输出为:

image.png

显然测试通过!

2.3 栈的应用

栈的应用场景对于我们并不陌生,如:

  • 软件的撤销(Undo)、回复(Redo)功能
  • 浏览器的前进、后退功能

2.4 练习:有效的括号

接下来用个简单的 leetcode算法题:20-有效的括号 练习一下

2.4.1 问题描述(摘自leetcode)

给定一个只包括 '('')''{''}''['']' 的字符串 s ,判断字符串是否有效。

  • 有效字符串需满足:
    • 左括号必须用相同类型的右括号闭合。
    • 左括号必须以正确的顺序闭合。
  • 示例:
输入:s = "()[]{}"
输出:true

输入:s = "([)]"
输出:false

输入:s = "{[]}"
输出:true

输入:s = "{[])}"
输出:false

2.4.2 解题思路

考虑到用栈解题,就必须想好入栈、出栈的操作,思路如下:

  1. 对字符串逐个字符遍历;
  2. 如果是左字符(([{),则入栈;
  3. 如果是右字符()]}),判断栈是否为空:
    • 如果栈是空的,说明括号无效,直接return false
    • 如果栈不为空,则弹出栈顶字符,与右字符匹配
      • 若结果匹配,继续扫描下一个字符
      • 若结果不匹配,说明括号无效,直接return false
  4. 所有字符扫描完毕后,如果:
    • 栈为空,说明括号有效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;
}

结果如下图:

image.png

测试通过!

3. 队列

队列是一种特殊的线性表,可以在两端进行操作,如图所示:

image.png

3.1 常见操作

队列的双端分别是队尾(rear)、队头(front),其常见操作如下:

  • 入队:往队列中添加元素的操作,叫做入队(即enqueue),如图所示:

image.png

入队的那一端叫做队尾(rear)

  • 出队:从队列中移除元素的操作,叫做出队(即dequeue),如图所示:

image.png

出队的那一端叫做队头(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.hQueue类遵循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]);
    }
}

运行后的输出为:

image.png

显然测试通过!

3.3 队列的应用

队列在日常生活中的应用非常广泛,凡是涉及到 排队 的,基本就是队列了,如:

  • 排队打疫苗
  • 在车站排队买票等

3.4 双端队列

双端队列:是能在头尾两端进行入队、出队的队列。其英文是dequedouble 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]);
    }
}

以及测试结果:

image.png

显然测试通过!

4 总结

来到这里,队列的相关内容就介绍完了。现在总结一下:

  • 队列都是特殊的线性表(底层可以由数组实现,也可以由链表实现)
  • 的特性是后进先出(LIFO)队列的特性是先进先出(FIFO)

由于这两种数据结构相似又各有不同,所以博主才将它们放在同一篇幅。按照惯例,接下来就以一二算法题作为练习吧。

4.1 练习1:用队列实现栈

用队列实现栈是leetcode的第225题,具体题目要求是:

  • 请你仅使用两个队列实现一个栈,并支持普通栈的全部四种操作:入栈(push)、出栈(pop)、获取栈顶元素(top)、是否为空(isEmpty)

4.1.1 实现思路

为了满足栈的特性(LIFO,即最后入栈的元素最先出栈),在使用队列实现栈时,最后入栈的元素必然是队头的元素。

由于题目要求用两个队列,我们先对它们进行编号,即queue1queue2,其中queue1作为主队列,用于存储入栈的元素,queue2作为入栈操作时的辅队列,每一次的栈操作,都要确保辅队列是个空队列(元素数量为0)。具体实现方案如下:

  1. 入栈时(假如入栈元素为a):
    • 把元素a入队到queue2(此时queue2中只有一个元素a)
    • queue1的所有元素依次出队,并入队到queue2
    • 交换queue1queue2

上述操作结束后,queue2依然是个空队列,queue1的队头、队尾分别对应栈顶、栈底

  1. 出栈时:直接调用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);
    }
}

测试结果如下:

image.png

显然测试通过!

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,一次倾倒就足以保证正序(队列的元素顺序),因此这里用inStackoutStack为两个栈命名,主要思路如下:

  1. 入队时(假如是元素a):将元素a入栈到inStack
  2. 出队时,遵循:
    • 如果outStack为空,把inStack中的所有元素逐一弹出,push到outStackoutStack弹出栈顶元素
    • 如果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]);
    }
}

测试结果如下:

image.png

显然测试通过!

5. 友情链接

6. 补充说明

  • 原文链接,转载请注明出处!
  • 文中所有代码都已上传到 github,感兴趣的同学可以去查看。