1. 概述
栈和队列都属于线性表,它们均可使用顺序或链式来实现。二者之间的比较:
栈与队列在开发中的实际应用场景:
1.1 栈
栈(Stack):一个有穷线性数据结构,具有后进先出(LIFO)的特性
栈中元素从栈顶(top)压入,也从栈顶弹出
栈的操作:
isFull:判断栈是否已满isEmpty:判断是否为空栈push:将元素压栈,添加元素到栈顶pop:将元素出栈,删除栈顶元素
新添加的元素永远存储在栈顶,而删除的也永远是栈顶元素,所以栈是后进先出的数据结构。而栈的top指针,只要是非空栈,它会永远指向栈顶元素
栈的top指针在压栈和出栈时,会跟随栈中的数据时时变化。如果是空栈,则top = -1
1.2 队列
队列(Queue):有穷线性数据结构,具有先进先出(FIFO)的特性
队列中的元素,从队尾(rear)入队,从队头(front)出队
队列的操作:
isFull:判断栈是否已满isEmpty:判断是否为空栈enqueue:将元素入队,添加到队列的队尾dequeue:将元素出队,删除队头的元素
一般来说,空队列的front和rear都为0,而非空队列front指向队头,rear指向队尾的下一个元素
当出队速度大于入队速度,会产生“假溢出”问题。使用循环队列,可以充分利用向量空间,克服“假溢出”现象
循环队列永远不能让它的头尾相遇,否则通过front == rear将无法区分队列是满或是空
正确的做法,可以牺牲队列中一个存储单元,当front == rear表示队列为空,而(rear + 1) % MAXSIZE == front,则表示队列已满
2. 用栈实现队列
请使用两个栈实现先进先出队列,只能使用标准的栈操作是合法的。队列应当支持一般队列支持的所有操作(push、pop、peek、empty)
实现MyQueue类:
void push(int x):将元素x推到队列的末尾int pop():从队列的开头移除并返回元素int peek():返回队列开头的元素boolean empty():如果队列为空,返回true。否则,返回false
示例:
MyQueue myQueue = new MyQueue();myQueue.push(1); // queue is: [1]myQueue.push(2); // queue is: [1, 2] (leftmost is front of the queue)myQueue.peek(); // return 1myQueue.pop(); // return 1, queue is [2]myQueue.empty(); // return false
使用pushStack和popStack两个栈,实现队列的特性。入队逻辑非常简单,永远只会往pushStack中push数据。而出队逻辑相对复杂,因为要符合队列先进先出的特性
让popStack负责pop操作,由于数据都被push到pushStack中,所以popStack为空栈,我们先将pushStack中的数据导入到popStack中。此时,数据在popStack中的顺序会改变,原本先入栈的数据会存储在栈顶
所以,只要popStack为空栈,我们就先从pushStack中导入数据,然后再pop数据。否则,只要popStack不为空栈,直接从中pop数据即可
切记,只要popStack为非空栈,就不能从pushStack中导入新的数据,因为会破坏popStack的先进先出结构
代码实现:
class MyQueue {fileprivate var pushStack : LinkedStack<Int>;fileprivate var popStack : LinkedStack<Int>;init() {pushStack = LinkedStack<Int>();popStack = LinkedStack<Int>();}func enter(x : Int){pushStack.push(element: x);}func out() -> Int?{let front = peek();if(front == nil){return front;}return popStack.pop();}func peek() -> Int?{if(isEmpty()){return nil;}if(popStack.isEmpty()) {while(!pushStack.isEmpty()){popStack.push(element: pushStack.pop()!);}}return popStack.getTop();}func isEmpty() -> Bool {if(popStack.isEmpty() && pushStack.isEmpty()){return true;}return false;}}
时间复杂度:入队操作O(1),出队操作O(1)
空间复杂度:O(n)
单次出队操作,可能需要先进行数据的导入。这种情况操作代价可能很大,但是最大执行次数跟它之前执行过入队操作的次数有关,并且在导入的数据全部出队之前,不会再触发这种最坏的情况。所以平均复杂度应为O(1)
3. 用队列实现栈
请使用两个队列实现一个后进先出(LIFO)的栈,并支持普通栈的全部四种操作(push、top、pop、empty)。
实现MyStack类:
void push(int x):将元素x压入栈顶int pop():移除并返回栈顶元素int top():返回栈顶元素boolean empty():如果栈是空的,返回true。否则,返回false
示例:
MyStack myStack = new MyStack();myStack.push(1);myStack.push(2);myStack.top(); // 返回 2myStack.pop(); // 返回 2myStack.empty(); // 返回 False
3.1 两个队列实现
定义queue1与queue2两个队列,当queue1为空队列,直接将数据入队queue1即可。而queue1为非空队列,先将数据入队queue2,然后再将queue1中的数据入队queue2,最后交换queue1和queue2的指针
此时,最后添加的数据在队头,而之前添加的数据,会被导入在队尾,这符合栈的后进先出结构
使用这种方式,有数据的一定只有queue1,所以top和pop操作,只需要针对queue1进行peek与out操作即可
代码实现:
class MyStack {fileprivate var queue1 : LinkedQueue<Int>;fileprivate var queue2 : LinkedQueue<Int>;init() {queue1 = LinkedQueue<Int>();queue2 = LinkedQueue<Int>();}func push(x : Int){if(queue1.isEmpty()){queue1.enter(element: x);return;}queue2.enter(element: x);while(!queue1.isEmpty()){queue2.enter(element: queue1.out()!);}let tmp = queue1;queue1 = queue2;queue2 = tmp;}func pop() -> Int?{let top = getTop();if(top == nil){return top;}return queue1.out();}func getTop() -> Int?{if(isEmpty()){return nil;}return queue1.peek();}func isEmpty() -> Bool {if(queue1.isEmpty()){return true;}return false;}}
时间复杂度:入栈操作O(n),其余操作O(1)
空间复杂度:O(n)
3.2 一个队列实现
我们需要记录添加到队列中的数据数n,有新数据入队,同时进行n + 1。入队后,将队列遍历n - 1次,将队列中的数据依次出队再入队,这样就会将队头的数据导入到队尾,同样实现了栈的后进先出结构
代码实现:
class MyStack {fileprivate var queue : LinkedQueue<Int>;init() {queue = LinkedQueue<Int>();}func push(x : Int){queue.enter(element: x);for _ in 0..<queue.length - 1 {queue.enter(element: queue.out()!);}}func pop() -> Int?{let top = getTop();if(top == nil){return top;}return queue.out();}func getTop() -> Int?{if(isEmpty()){return nil;}return queue.peek();}func isEmpty() -> Bool {if(queue.isEmpty()){return true;}return false;}}
时间复杂度:入栈操作O(n),其余操作O(1)
空间复杂度:O(n)
4. 自动释放池
自动释放池是一个存储指针的栈结构,指针要么是一个要释放的对象,要么是自动释放池的边界,俗称:哨兵对象。自动释放池的栈空间,被分成一个双向链接结构的页面列表,可添加和删除页面
栈原则,后进先出。对象最后被
push,最先被pop双向链表的特点,一个页中同时存在父结点和子结点。可向前找到父页面,也可向后找到子页面
4.1 定义默认值
#import <Foundation/Foundation.h>#include <pthread.h>#include <malloc/malloc.h>#import <objc/runtime.h>// 哨兵对象#define POOL_BOUNDARY nil// 线程局部存储的keystatic pthread_key_t const key = 39;// 空池占位符#define EMPTY_POOL_PLACEHOLDER ((id*)1)// 自动释放池中对象销毁,使用0xA3填充static uint8_t const SCRIBBLE = 0xA3; // 0xA3A3A3A3 after releasing// 自动释放池一页预设大小static size_t const SIZE = PAGE_MIN_SIZE;
4.2 页的数据结构
@class AutoreleasePoolPage;@interface AutoreleasePoolPageData : NSObject {// 存储对象的指针域@package __unsafe_unretained id* _next;// 父页面指针域AutoreleasePoolPage *_parent;// 子页面指针域AutoreleasePoolPage *_child;}@end@implementation AutoreleasePoolPageData@end
4.3 页的初始化
@interface AutoreleasePoolPage : AutoreleasePoolPageData@end@implementation AutoreleasePoolPage// 开辟空间+ (instancetype)allocWithZone:(struct _NSZone *)zone {// 起始地址为4k整数倍 + 4kvoid *as = malloc_zone_memalign(malloc_default_zone(), SIZE, SIZE);// 用来对一段内存空间全部设置为某个字符。使用0填充as首地址之后4k长度memset(as, 0, SIZE);// 开辟空间void *src = [super allocWithZone:zone];// 正常开辟空间,我们无法控制对象所在地址,但我们可以在它开辟后,将其拷贝到指定空间中// 从src空间中移动n个元素,将其赋值给as的内存中。从内存的第一个地址所指向的数据开始复制,直到赋值n个数据// 该函数也就调用结束,同时返回as// 如果源区域和目的区域有重叠部分的话,可以在源区域未被目的区域覆盖的时候拷贝到目的区域中memmove(as, src, sizeof(src));// 拷贝后将src释放[(id)src release];return as;}// 初始化- (instancetype)initWithPage:(AutoreleasePoolPage *)page {self = [super init];if (self) {// 将next默认设置为:当前对象首地址 + 对象大小_next = [self begin];// 连接双向链表,设置当前对象的父结点_parent = page;if (_parent) {// 将父结点的子结点设置为自身_parent->_child = self;}}return self;}// 当前页的起始地址,结构体首地址 + 结构体大小- (id *)begin {return (id *)((uint8_t *)self + class_getInstanceSize(self.class));}// 当前页的结束地址,结构体首地址 + 预设大小- (id *)end {return (id *) ((uint8_t *)self + SIZE);}// next指向结束地址,当前页已满- (BOOL)full {return _next == [self end];}// next指向起始地址,当前页为空- (BOOL)empty {return _next == [self begin];}@end
4.4 将对象添加至自动释放池
@implementation AutoreleasePoolPage// 添加哨兵对象(边界)+ (void *)push {// 添加哨兵对象到自动释放池,返回哨兵对象在池子中的地址id *dest = [self autoreleaseFast:POOL_BOUNDARY];// 返回的地址不是空池占位符,并且里面存储的不是哨兵对象,程序终止assert(dest == EMPTY_POOL_PLACEHOLDER || *dest == POOL_BOUNDARY);// 返回哨兵对象的地址return dest;}// 添加对象+ (id)autorelease:(id)obj {// 对象为空,程序终止assert(obj);// 添加对象到自动释放池,返回对象在池子中的地址id *dest __unused = [self autoreleaseFast:obj];// 返回地址非空,并且地址不是空池占位符,并且里面存储的不是传入的对象,程序终止assert(!dest || dest == EMPTY_POOL_PLACEHOLDER || *dest == obj);// 返回传入的对象return obj;}// 将对象添加到自动释放池- (id *)add:(id)obj {// 当前页已满,程序终止assert(!self.full);// 得到当前next地址id *ret = _next;// 将对象存储在next中*_next = obj;// next向下平移_next++;// 返回存储对象的地址return ret;}@end
添加对象到自动释放池:
@implementation AutoreleasePoolPage// 快速创建自动释放池+ (id *)autoreleaseFast:(id)obj {// 获取热页AutoreleasePoolPage *page = [self hotPage];if (page && ![page full]) {// 存在,并且未满,将对象加入自动释放池return [page add:obj];} else if (page) {// 存在,并且已满,先添加新页,再添加对象return [self autoreleaseFullPage:obj page:page];} else {// 不存在,先创建,然后添加对象return [self autoreleaseNoPage:obj];}}// 创建自动释放池,然后添加数据+ (id *)autoreleaseNoPage:(id)obj {// 存在热页,终止程序assert(!self.hotPage);// 加入哨兵对象的标记,默认为falseBOOL pushExtraBoundary = false;// 判断当前线程是否已存在空池占位符if (self.haveEmptyPoolPlaceholder) {// We are pushing a second pool over the empty placeholder pool// or pushing the first object into the empty placeholder pool.// Before doing that, push a pool boundary on behalf of the pool// that is currently represented by the empty placeholder.// 已存在,先插入哨兵对象,修改标记为truepushExtraBoundary = true;}else if (obj == POOL_BOUNDARY) {// We are pushing a pool with no pool in place,// and alloc-per-pool debugging was not requested.// Install and return the empty pool placeholder.// 不存在,且当前插入的对象为哨兵对象,当前线程先创建一个空池占位符return [self setEmptyPoolPlaceholder];}// 创建一个新页,当前新页为根结点,所以父结点为nilAutoreleasePoolPage *page = [[AutoreleasePoolPage alloc] initWithPage:nil];// 将当前新页设置为热页[self setHotPage:page];// 判断插入哨兵对象的标记if (pushExtraBoundary) {// 标记为true,插入哨兵对象[page add:POOL_BOUNDARY];}// Push the requested object or pool.// 将对象加入自动释放池return [page add:obj];}// 热页已满,先添加新页,再添加对象+ (id *)autoreleaseFullPage:(id)obj page:(AutoreleasePoolPage *)page {// The hot page is full.// Step to the next non-full page, adding a new page if necessary.// Then add the object to that page.// 当前传入的页不是热页,程序终止assert(page == self.hotPage);// 当前页未满,程序终止assert([page full]);do {// 判断当前页是否存在子页if (page->_child) {// 存在,继续向下找page = page->_child;}else {// 不存在,创建新页,传入当前页作为父结点,新页为当前页的子页page = [[self alloc] initWithPage:page];}// 如果找到未写满的页,停止循环} while ([page full]);// 将未写满的页设置为热页[self setHotPage:page];// 将对象加入自动释放池return [page add:obj];}@end
空池占位符:
@implementation AutoreleasePoolPage// 当前线程是否已存在空池占位符+ (BOOL)haveEmptyPoolPlaceholder {// 从线程局部存储中获取id *tls = (id *)pthread_getspecific(key);// 结果为空池占位符,返回truereturn (tls == EMPTY_POOL_PLACEHOLDER);}// 当前线程创建一个空池占位符+ (id*)setEmptyPoolPlaceholder {// 线程局部存储中有值,程序中断assert(pthread_getspecific(key) == nil);// 将空池占位符写入线程局部存储中pthread_setspecific(key, (void *)EMPTY_POOL_PLACEHOLDER);// 返回空池占位符return EMPTY_POOL_PLACEHOLDER;}// 获取冷页+ (AutoreleasePoolPage *)coldPage {// 获取热页AutoreleasePoolPage *result = self.hotPage;// 判断结果是否为空if (result) {// 结果非空,一直遍历到根结点while (result->_parent) {result = result->_parent;}}// 返回结果return result;}//获取热页+ (AutoreleasePoolPage *)hotPage {// 从线程局部存储中获取AutoreleasePoolPage *result = (AutoreleasePoolPage *)pthread_getspecific(key);// 返回的结果为空池占位符,返回nilif ((id *)result == EMPTY_POOL_PLACEHOLDER) return nil;// 返回结果return result;}// 设置热页+ (void)setHotPage:(AutoreleasePoolPage *)page {// 写入线程局部存储中pthread_setspecific(key, page);}@end
冷页 & 热页:
@implementation AutoreleasePoolPage// 获取冷页+ (AutoreleasePoolPage *)coldPage {// 获取热页AutoreleasePoolPage *result = self.hotPage;// 判断结果是否为空if (result) {// 结果非空,一直遍历到根结点while (result->_parent) {result = result->_parent;}}// 返回结果return result;}//获取热页+ (AutoreleasePoolPage *)hotPage {// 从线程局部存储中获取AutoreleasePoolPage *result = (AutoreleasePoolPage *)pthread_getspecific(key);// 返回的结果为空池占位符,返回nilif ((id *)result == EMPTY_POOL_PLACEHOLDER) return nil;// 返回结果return result;}// 设置热页+ (void)setHotPage:(AutoreleasePoolPage *)page {// 写入线程局部存储中pthread_setspecific(key, page);}@end
4.5 将对象从自动释放池中移除
@implementation AutoreleasePoolPage// 移除并销毁对象+ (void)pop:(void *)token {AutoreleasePoolPage *page;id *stop;// 判断传入的地址是否等于空池占位符if (token == (void*)EMPTY_POOL_PLACEHOLDER) {// Popping the top-level placeholder pool.// 当前地址为空池占位符,获取热页page = self.hotPage;// 热页不存在if (!page) {// Pool was never used. Clear the placeholder.// 将其标记为nilreturn [self setHotPage:nil];}// Pool was used. Pop its contents normally.// Pool pages remain allocated for re-use as usual.// 获取冷页(根结点)page = self.coldPage;// 记录根结点页面的起始地址token = page.begin;} else {// 获取地址所属页的首地址page = [self pageForPointer:(uintptr_t)token];}// 将stop赋值stop = (id *)token;// 将对象依次销毁,到指定位置结束return [self popPage:token page:page stop:stop];}@end
获取地址所属页的首地址:
@implementation AutoreleasePoolPage// 获取地址所属页的首地址+ (AutoreleasePoolPage *)pageForPointer:(uintptr_t)p {AutoreleasePoolPage *result;// 当前地址和4K取余,得到偏移量,也就是已使用的容量uintptr_t offset = p % SIZE;// 如果偏移量小于AutoreleasePoolPage类的容量,程序终止assert(offset >= sizeof([AutoreleasePoolPage class]));// 使用当前地址 - 偏移量,得到页的首地址result = (AutoreleasePoolPage *)(p - offset);// 返回结果return result;}@end
将对象依次销毁,到指定位置结束:
@implementation AutoreleasePoolPage// 将对象依次销毁,到指定位置结束+ (void)popPage:(void *)token page:(AutoreleasePoolPage *)page stop:(id *)stop {// 将对象依次销毁,到指定位置结束[page releaseUntil:stop];// 判断当前页是否存在子页if (page->_child) {// hysteresis: keep one empty child if page is more than half full// 存在子页,判断当前页使用的容量是否小于一半if (page.lessThanHalfFull) {// 小于一半,释放当前页之后的全部子页[page->_child kill];}else if (page->_child->_child) {// 超过一半,仅保留一个子页,释放子页之后的全部子页[page->_child->_child kill];}}}// 判断当前页的使用量不满一半- (BOOL)lessThanHalfFull {// 已使用地址 - 起始地址 < (结束地址 - 起始地址) / 2return (_next - self.begin < (self.end - self.begin) / 2);}// 将对象依次销毁,到指定位置结束- (void)releaseUntil:(id *)stop {// Not recursive: we don't want to blow out the stack// if a thread accumulates a stupendous amount of garbage// next不等于指定地址,持续遍历while (self->_next != stop) {// Restart from hotPage() every time, in case -release// autoreleased more objects// 获取热页AutoreleasePoolPage *page = self.class.hotPage;// fixme I think this `while` can be `if`, but I can't prove it// 判断当前热页是否为空while (page.empty) {// 已经是空页,说明当前页中的数据全部销毁,但还未达到指定位置// 获取父页,继续销毁父页中的对象page = page->_parent;// 将其设置为新的热页[self.class setHotPage:page];}// next向上平移page->_next--;// 获取next中的对象id obj = *page->_next;// 将next存储的数据使用0xA3填充memset((void*)page->_next, SCRIBBLE, sizeof(*page->_next));// 判断当前对象是否为哨兵对象if (obj != POOL_BOUNDARY) {// 不是哨兵对象,调用它的release方法[obj release];}}// 将当前页设置为热页[self.class setHotPage:self];}@end
销毁当前页以及之后的子页:
@implementation AutoreleasePoolPage// 销毁当前页以及之后的子页- (void)kill{// Not recursive: we don't want to blow out the stack// if a thread accumulates a stupendous amount of garbage// 将当前页赋值给临时变量AutoreleasePoolPage *page = self;// 遍历,找到最后一个子页while (page->_child) {page = page->_child;}AutoreleasePoolPage *deathptr;do {// 从最后的子页开始销毁,一直到当前页停止deathptr = page;page = page->_parent;if (page) {page->_child = nil;}[deathptr release];} while (deathptr != self);}@end
4.6 入口函数
// 添加边界void * objc_autoreleasePoolPush(void) {return [AutoreleasePoolPage push];}// 将对象添加至自动释放池id objc_autorelease(id obj) {return [AutoreleasePoolPage autorelease:obj];}// 将对象从自动释放池中移除void objc_autoreleasePoolPop(void * _Nonnull context) {return [AutoreleasePoolPage pop:context];}
总结
概述:
栈(
Stack):一个有穷线性数据结构,具有后进先出(LIFO)的特性- 栈中元素从栈顶(
top)压入,也从栈顶弹出
- 栈中元素从栈顶(
队列(
Queue):有穷线性数据结构,具有先进先出(FIFO)的特性- 队列中的元素,从队尾(
rear)入队,从队头(front)出队
- 队列中的元素,从队尾(
用栈实现队列:
使用
pushStack和popStack两个栈,实现队列的特性。入队逻辑非常简单,永远只会往pushStack中push数据。而出队逻辑相对复杂,因为要符合队列先进先出的特性让
popStack负责pop操作,由于数据都被push到pushStack中,所以popStack为空栈,我们先将pushStack中的数据导入到popStack中。此时,数据在popStack中的顺序会改变,原本先入栈的数据会存储在栈顶所以,只要
popStack为空栈,我们就先从pushStack中导入数据,然后再pop数据。否则,只要popStack不为空栈,直接从中pop数据即可切记,只要
popStack为非空栈,就不能从pushStack中导入新的数据,因为会破坏popStack的先进先出结构
用队列实现栈:
两个队列实现:定义
queue1与queue2两个队列,当queue1为空队列,直接将数据入队queue1即可。而queue1为非空队列,先将数据入队queue2,然后再将queue1中的数据入队queue2,最后交换queue1和queue2的指针一个队列实现:我们需要记录添加到队列中的数据数
n,有新数据入队,同时进行n + 1。入队后,将队列遍历n - 1次,将队列中的数据依次出队再入队,这样就会将队头的数据导入到队尾,同样实现了栈的后进先出结构
自动释放池:
是一个存储指针的栈结构,指针要么是一个要释放的对象,要么是自动释放池的边界,俗称:哨兵对象。自动释放池的栈空间,被分成一个双向链接结构的页面列表,可添加和删除页面
栈原则,后进先出。对象最后被
push,最先被pop双向链表的特点,一个页中同时存在父结点和子结点。可向前找到父页面,也可向后找到子页面
