栈和队列

614 阅读4分钟

栈是一种受到限制和线性表,只能够在表尾添加元素,并且只能够获取和删除表尾元素。表尾成为栈的栈顶,表头成为栈的栈底。向栈添加一个元素成为进栈、入栈或压栈,取出元素称为出栈或退栈。

完全可以把栈理解成为一个功能受到限制的链表:只能够向链表的尾部追加元素,并且只能够获取和删除链表的尾部元素。链表的尾部称为栈顶,链表的头部成为栈底。入栈即向链表尾部添加一个元素,出栈即移除尾部元素。

假如依次向栈压入三个元素:Rose、Jack、Whip,入栈操作如下图:

而出栈即每次都去栈顶元素,如上图出栈的顺序应该是:Whip、Jack、Rose,刚好和入栈顺序相反。

既然栈的功能完全就是一个功能受限的线性表,我们就可以封装一个线性表实现一个自定义栈。之前我们已经自定义实现了5种对外接口和功能完全一致的线性表结构:动态数组单向链表单向循环链表双向链表双向循环链表

既然栈只需要对线性表的尾部进行添加和删除操作,动态数组双向链表双向循环链表对尾部的操作时间复杂度都是O(1),单向链表单向循环链表对尾部的操作时间复杂度都是O(n)。栈也不需要使用到双向循环链表的特性,使用双向链表动态数组都是合适的。

栈的接口定义

@interface JKRStack<ObjectType> : NSObject

/// 返回栈的元素个数
- (NSUInteger)count;
/// 入栈
- (void)push:(nullable ObjectType)anObject;
/// 出栈
- (ObjectType)pop;
/// 获取栈顶元素
- (ObjectType)peek;

@end

栈内部封装一个线性表,这个线性表使用懒加载,栈的类型这里定义成所有线性表的父类,由于之前封装的所有线性表结构和功能都是一样的,后面可以方便的修改懒加载返回的子类类型,来测试不同类型的线性表实现的时间复杂度对比。

@interface JKRStack ()

@property (nonatomic, strong) JKRBaseList *array;

@end

- (JKRBaseList *)array {
    if (!_array) {
         _array = [JKRLinkedList new];
    }
    return _array;
}

栈的接口实现

栈的元素个数

- (NSUInteger)count {
    return self.array.count;
}

入栈

- (void)push:(id)anObject {
    [self.array addObject:anObject];
}

出栈

- (id)pop {
    [self rangeCheck];
    id object = self.array.lastObject;
    [self.array removeLastObject];
    return object;
}

获取栈顶

- (id)peek {
    [self rangeCheck];
    return self.array.lastObject;
}

边界检查

- (void)rangeCheck {
    if (self.array.count == 0) {
        NSAssert(NO, @"stack is empty");
    }
}

栈功能测试

将 0 - 9999 分别入栈,然后依次弹出栈顶直到栈为空:

[JKRTimeTool teskCodeWithBlock:^{
    JKRStack *stack = [JKRStack new];
    for (NSUInteger i = 0; i < 10000; i++) {
        [stack push:[NSNumber numberWithInteger:i]];
    }
    
    while (stack.count) {
        NSLog(@"%@",[stack pop]);
    }
}];

打印结果:

9999
9998
...
1
0
耗时: 0.170 s

时间对比

修改线性表的懒加载返回的对象,对比不同线性表的时间:

- (JKRBaseList *)array {
    if (!_array) {
        _array = [JKRLinkedList new];
//        _array = [JKRLinkedCircleList new];
//        _array = [JKRSingleLinkedList new];
//        _array = [JKRSingleCircleLinkedList new];
//        _array = [JKRArrayList new];
    }
    return _array;
}

进行 100000次 入栈和出栈,时间对比对比如下:

// JKRLinkedList(双向链表)
耗时: 0.009 s

// JKRLinkedCircleList(双向循环链表)
耗时: 0.011 s
 
// JKRSingleLinkedList(单向链表)
耗时: 2.880 s

// JKRSingleCircleLinkedList(单向循序链表)
耗时: 2.888 s

// JKRArrayList(动态数组)
耗时: 0.004 s

可以发现,同样的接口和功能的线性表,在内部实现方式不同的情况下,差别非常的大,所以在实现一个功能的时候,需要考虑数据结构的特性,选择最优的方案。

队列

队列和栈非常相似,也是功能受限的线性表,只能够在尾部添加元素,并且只能够从头部获取和删除元素。从尾部添加元素称为入队,从头部取出元素称为出队。

基于队列的性质,队列满足先进先出的原则,像一群人排队进入关口一样,先入队的人在队头最先出去,后入队的人在队尾,要等前面的人都出去了才能轮到他出去。

既然队列需要在队尾进行添加,队头进行取元素和删除,需要同时对头部和尾部进行操作,使用双向链表是最合适的。

队列的接口定义

@interface JKRQueue<ObjectType> : NSObject

/// 队列的元素个数
- (NSUInteger)count;
/// 入队
- (void)enQueue:(nullable ObjectType)anObject;
/// 出队
- (ObjectType)deQueue;
/// 获取队头
- (ObjectType)front;

@end

队列内部需要封装一个线性表,这里使用双向链表

@interface JKRQueue ()

@property (nonatomic, strong) JKRBaseList *array;

@end

@implementation JKRQueue

- (JKRBaseList *)array {
    if (!_array) {
        _array = [JKRLinkedList new];
    }
    return _array;
}

@end

队列的接口实现

队列的元素个数

- (NSUInteger)count {
    return self.array.count;
}

入队

- (void)enQueue:(id)anObject {
    [self.array addObject:anObject];
}

出队

- (id)deQueue {
    [self rangeCheck];
    id object = self.array.firstObject;
    [self.array removeFirstObject];
    return object;
}

获取队头

- (id)front {
    [self rangeCheck];
    return self.array.firstObject;
}

边界检查

- (void)rangeCheck {
    if (self.array.count == 0) {
        NSAssert(NO, @"queue is empty");
    }
}

接下来

有了栈和队列的概念,后面就可以为二叉树的操作做好准备了。