因为这个解法有点复杂,因此单独开一贴介绍。

那么这里就使用六个栈来解决这个问题。

这个算法来自于这篇论文

原文里用的是 Pure Lisp,不过语法很简单,还是很容易看懂的。

先导知识——用两个栈模拟一个队列

如何使用两个栈来模拟一个队列操作?

这是一道很经典的题目,答案也有很多种,这里只介绍之后会用到的一种方法。

首先我们有两个栈,H 和 T,分别用作出队和入队用。

这样,入队操作等同于向 T 添加元素,T 的入栈操作只需要 O(1) 时间。

如果 H 不为空,出队操作等同于 H 弹栈,H 的弹栈操作也只需要 O(1) 时间。

但如果 H 为空,则需要将 T 中的元素依次弹出并压入到 H 中,这是一个 O(n) 的操作。

显然,这种方式中,出队操作的最坏时间复杂度是 O(n),并不满足题目要求。

分摊 O(n)

那么,怎么解决这个问题呢?

一个很自然的想法是,如果在栈 H 变为空之前,我们就能逐步将栈 T 的内容弹出并压入到另一个栈 H' 中,等到栈 H 为空时,直接交换 H 和 H' 即可。

假设目前的队列状态是这样,有三个元素等待出队,还有三个元素等待入队。

现在依次让三个元素出队,与此同时我们让栈 T 中的元素依次进入 H' 中。

每一次出队都执行两个操作,元素出队和元素复制(Pop & Push),时间复杂度 O(1) + O(1) + O(1) = O(1)。

第一次操作(出队)

第二次操作(出队)

第三次操作(出队)

现在栈 H 和栈 T 都为空,下一次出队操作时,我们直接交换栈 H 和栈 H'(由于是交换引用,因此时间复杂度仍为 O(1))。

之后再进行出队操作。

这就是这个算法基本想法,在栈 H 变为空之前,分步将栈 T 中的内容分步复制到另一个栈中。

当栈 H 为空时直接用准备好的栈 H' 替代 H,保证时间复杂度为常数。

对复制时 Enqueue 的支持和 T' 的引入

刚才是一种理想情况,显然我们的队列在复制时不可能只发生出队操作,为了增加对入队操作的支持,我们引入临时栈 T'。

例如我们有队列状态如下,现在启动复制进程,入队操作全部由 T' 完成。

我们进行一次入队操作和两次出队操作,如下组图所示:

第一次操作(入队)

第二次操作(出队)

第三次操作(出队)

现在 H 和 T 均为空,下一次操作时(不论入队还是出队),我们先交换 H 和 H' 以及 T 和 T',同时让入队操作控制权回到 T。

这样,我们增加了对复制时入队操作的支持,但还并不完全,只有在理想情况下才可以做到。

h 与 HR ,对复制时出入队序列支持的扩展

在之前的例子中,当复制结束时 H 总是为空的,现在我们来讨论一下复制结束时 H 不为空的情况。

如果复制结束时 H 不为空,直接交换的结果是我们丢失了原来栈 H 中的数据。

因此,在翻转 T 的同时,我们还应翻转 H 到 HR,并在最后将 HR 的内容再度翻转并添加到 H' 上。

这个过程可以以下图方式进行:

初始状态:

第一次操作(入队),H->HR ,T->H',时间复杂度 O(1) + O(1) + O(1) + O(1) + O(1) = O(1)。

第二次操作(入队)

第三次操作(入队)

第四次操作(入队)

第五次操作(入队)

第六次操作(出/入队执行前)

这样我们就解决了 H 复制结束后不为空的问题,代价是引入了两个额外的问题:

  1. 操作次数增加到了 2k 次,k 代表栈 T 中的元素数量。(如果当 T 中元素数量大于 H 中元素数量时开始复制)
  2. 由于 H 被用于复制进程,我们无法在复制过程中支持出队操作。

第一个问题解决方案比较简单,我们可以在每一次出/入队操作执行时进行两次的复制步骤(对 T 和 H 进行两次的 Pop 操作),时间复杂度仍为 O(1)。

第二个问题我们通过引入栈 h 来解决。

h 用于在复制时代替 H 执行出队功能,它会在复制开始时自动变为栈 H 的一个浅拷贝(也就是说,h 和 H 共用同一片内存空间,但它们用于指示栈顶位置的指针相互独立)。

现在我们有了全部 6 个栈,它们的功能如下图所示(为了方便介绍我将一些栈的位置做了调换)。

由于我们并不能预知接下来会发生的操作,因此当 H 栈中的元素数量第一次小于 T 栈中的元素数量时,我们就必须启动复制进程了(总是假设接下来全部都是出队操作)。我们引入一个布尔类型变量 IsCopying 来指示复制进程。

现在我们进行第一次入队操作,IsCopying = true,开始复制。

首先 h 变为 H 的浅拷贝,这个过程是 O(1) 的。

如果在复制过程中有出队操作,作为 H 的翻转 HR 中就有一个元素不再需要复制,我们引入一个变量 needcopy 来记录 HR 中需要复制的元素数量。

接下来是两次复制操作,T 和 H 分别有两个元素进入了 H' 和 HR

然后是第二次出/入队操作,这次我们选择出队,1 出队后显然 HR 中的 1 不再需要复制,needcopy – 1。

随后再是两次复制操作,第一次将 H 中的 3 移到 HR 中,needcopy + 1,T 中的 5 移到 H' 中;第二次只将 T 中的 4 移到 H' 中。

第三次出/入队操作我们选择入队,8 入队。随后 HR 中的两个元素进入了 H',needcopy – 2。

由于 needcopy 变成了 0,我们再额外进行一次交换操作,并将 IsCopying 置为 false。

至此,完整的算法运行完毕。

有关复制开始时机的证明

这里我们选择了在第 k + 1 个元素入队时开始复制,现在证明一定能够在 h 空之前完成复制:

假设复制开始时 H 有 k 个元素,T 有 k + 1个元素。

完成第一轮复制(H->HR , T->H')需要 k + 1 次操作,

完成第二轮复制(H->H')需要 k 次操作,总共需要 2k + 1 次操作才能完成复制。

而 h 的长度为 k,能够提供 2k 次的操作机会。第 k + 1 个元素入队时也能提供 2 次操作机会,因此一共是 2k + 2 次操作机会。

由于 2k + 1 < 2k + 2,我们证明了该算法能够及时完成复制工作。

程序设计

根据之前的内容,我们可以开始设计程序了。主要实现三个功能,Enqueue(), Dequeue() 和 Peek()。

根据算法要求我们添加一个进行复制时操作的函数 OneStep(),用于执行元素的复制,栈交换等操作。

Peek() 只需要根据是否在进行复制选择栈 h 或栈 H 进行 Peek()。

Enqueue()

  1. 如果不处于复制状态
    1. 如果 H.Length – T.Length > 0,直接将元素压入栈 T。
    2. 否则令 IsCopying = true,h 进行浅拷贝,进行两次的 OneStep。
  2. 如果处于复制状态,将元素压入 T',进行两次的 OneStep。

Dequeue()

  1. 如果不处于复制状态
    1. 如果 H.Length – T.Length > 0,直接从 H 弹出元素。
    2. 否则从 H 弹出元素,IsCopying = true,h 进行浅拷贝,进行两次的 OneStep。
  2. 如果处于复制状态,从 h 弹出元素,needcopy - 1,进行两次的 OneStep。

OneStep()

  1. 如果不处于复制状态,什么也不做。
  2. 如果处于复制状态。
    1. 如果 H 和 T 都不为空,从 H 搬运一个元素至 HR ,从 T 搬运一个元素至 H' ,needcopy + 1。
    2. 如果 H 为空但 T 不为空,从 T 搬运一个元素至 H' 。
    3. 如果 H 和 T 都为空,但 needcopy > 1,从 HR 搬运一个元素至 H' ,needcopy – 1。
    4. 如果 H 和 T 都为空,但 needcopy = 1,从 HR 搬运一个元素至 H' ,needcopy – 1,交换 H 和 H' 以及 T 和 T',其他栈置空,退出复制状态。
    5. 如果 H 和 T 都为空,但 needcopy = 0,交换 H 和 H' 以及 T 和 T',其他栈置空,退出复制状态。

程序实现(C#)

显然光演示是不够的,这里放上我自己写的 C# 代码供参考(HH = H', TT = T'):

using Generics;

namespace _1._3._49
{
class StackQueue<Item>
{
Stack<Item> H;
Stack<Item> T;
Stack<Item> h;
Stack<Item> HH;
Stack<Item> TT;
Stack<Item> Hr; bool isRecopying;
int nowcopying; public StackQueue()
{
this.isRecopying = false;
this.nowcopying = ; this.H = new Stack<Item>();
this.T = new Stack<Item>();
this.h = new Stack<Item>();
this.HH = new Stack<Item>();
this.TT = new Stack<Item>();
this.Hr = new Stack<Item>();
} public Item Peek()
{
if (this.isRecopying)
{
return h.Peek();
}
else
{
return H.Peek();
}
} public void Enqueue(Item item)
{
if (!this.isRecopying && Lendiff() > )
{
this.nowcopying = ;
this.T.Push(item);
}
else if (!this.isRecopying && Lendiff() == )
{
this.T.Push(item);
this.isRecopying = true;
this.h = this.H.Copy();
OneStep(OneStep(this));
}
else if (this.isRecopying)
{
this.TT.Push(item);
OneStep(OneStep(this));
}
} public int Lendiff()
{
return this.H.Size() - this.T.Size();
} public Item Dequeue()
{
if (!this.isRecopying && Lendiff() > )
{
return this.H.Pop();
}
else if (!this.isRecopying && Lendiff() == )
{
Item temp = this.H.Pop();
this.h = this.H.Copy();
this.isRecopying = true;
OneStep(OneStep(this));
return temp;
}
else
{
Item temp = this.h.Pop();
this.nowcopying--;
OneStep(OneStep(this));
return temp;
}
} private static StackQueue<Item> OneStep(StackQueue<Item> q)
{
if (q.isRecopying && !q.H.IsEmpty() && !q.T.IsEmpty())
{
q.nowcopying++;
q.HH.Push(q.T.Pop());
q.Hr.Push(q.H.Pop());
}
else if (q.isRecopying && q.H.IsEmpty() && !q.T.IsEmpty())
{
q.isRecopying = true;
q.HH.Push(q.T.Pop());
}
else if (q.isRecopying && q.H.IsEmpty() && q.T.IsEmpty() && q.nowcopying > )
{
q.isRecopying = true;
q.nowcopying--;
q.HH.Push(q.Hr.Pop());
}
else if (q.isRecopying && q.H.IsEmpty() && q.T.IsEmpty() && q.nowcopying == )
{
q.isRecopying = false;
q.nowcopying--;
q.HH.Push(q.Hr.Pop());
q.H = q.HH;
q.T = q.TT;
q.HH = new Stack<Item>();
q.TT = new Stack<Item>();
q.Hr = new Stack<Item>();
q.h = new Stack<Item>();
}
else if (q.isRecopying && q.H.IsEmpty() && q.T.IsEmpty() && q.nowcopying == )
{
q.isRecopying = false;
q.H = q.HH;
q.T = q.TT;
q.HH = new Stack<Item>();
q.TT = new Stack<Item>();
q.Hr = new Stack<Item>();
q.h = new Stack<Item>();
}
return q;
}
}
}

后记

事实上这道题还没有结束,在英文版的《算法(第四版)》中,这道题要求的是只使用 3 个栈而不是有限个,具体可以查看 Stackoverflow 上的讨论

讨论的结果是 6 个栈显然可行,3 个栈的解决方案用到了懒加载技术,还有一种 3 栈方案太过取巧(3 个“栈的栈“),最后还有人试图证明 3 栈方案根本不可能。

显然后来作者意识到了问题因此改成了有限个栈,那么三个栈实现 O(1) 队列是否可能呢?

反正我是不知道怎么弄,尤其是看完 6 个栈的实现方案之后。 ╮(╯_╰)╭

算法(第四版)C# 习题题解——1.3.49 用 6 个栈实现一个 O(1) 队列的更多相关文章

  1. 算法(第四版)C#题解——2.1

    算法(第四版)C#题解——2.1   写在前面 整个项目都托管在了 Github 上:https://github.com/ikesnowy/Algorithms-4th-Edition-in-Csh ...

  2. 算法第四版 在Eclipse中调用Algs4库

    首先下载Eclipse,我选择的是Eclipse IDE for Java Developers64位版本,下载下来之后解压缩到喜欢的位置然后双击Eclipse.exe启动 然后开始新建项目,File ...

  3. 算法第四版jar包下载地址

    算法第四版jar包下载地址:https://algs4.cs.princeton.edu/code/

  4. 算法第四版-文字版-下载地址-Robert Sedgewick

    下载地址:https://download.csdn.net/download/moshenglv/10777447 算法第四版,文字版,可复制,方便copy代码 目录: 第1章 基 础 ...... ...

  5. 二项分布。计算binomial(100,50,0.25)将会产生的递归调用次数(算法第四版1.1.27)

    算法第四版35页问题1.1.27,估计用一下代码计算binomial(100,50,0.25)将会产生的递归调用次数: public static double binomial(int n,int ...

  6. 算法第四版学习笔记之优先队列--Priority Queues

    软件:DrJava 参考书:算法(第四版) 章节:2.4优先队列(以下截图是算法配套视频所讲内容截图) 1:API 与初级实现 2:堆得定义 3:堆排序 4:事件驱动的仿真 优先队列最重要的操作就是删 ...

  7. 算法第四版学习笔记之快速排序 QuickSort

    软件:DrJava 参考书:算法(第四版) 章节:2.3快速排序(以下截图是算法配套视频所讲内容截图) 1:快速排序 2:

  8. C程序设计(第四版)课后习题完整版 谭浩强编著

    //复习过程中,纯手打,持续更新,觉得好就点个赞吧. 第一章:程序设计和C语言 习题 1.什么是程序?什么是程序设计? 答:程序就是一组计算机能识别和执行的指令.程序设计是指从确定任务到得到结果,写出 ...

  9. 算法第四版 coursera公开课 普林斯顿算法 ⅠⅡ部分 Robert Sedgewick主讲《Algorithms》

    这是我在网上找到的资源,下载之后上传到我的百度网盘了. 包含两部分:1:算法视频的种子 2:字幕 下载之后,请用迅雷播放器打开,因为迅雷可以直接在线搜索字幕. 如果以下链接失效,请在下边留言,我再更新 ...

随机推荐

  1. ie清理缓存

    说废话,直接上图. 1.打开浏览器 2.工具--->Internet选项 3.常规--->设置 4.Internet临时文件--->查看文件 5.将缓存文件夹中内容全部删除

  2. ERROR 1075 (42000): Incorrect table definition; there can be only one auto column and it must be defined as a key

    约束字段为自动增长,被约束的字段必须同时被key约束 没有设置成primary key 时,会报错. 加上primary key 则设置成功.

  3. C++11 新特性之operator "" xxx

    从C++11开始,我们可以使用以下形式通过常量字符串构造自定义类型, 比如: class Person { public: Person(const std::string& name): _ ...

  4. MVC Views文件夹下js无法访问问题解决方案

    出现这个问题是因为webconfig做的限制,可修改相应Views下的webconfig文件来解决. <system.webServer> <handlers> <rem ...

  5. java中使用JDBC的preparedStatement批处理数据的添加

    在项目中我们偶尔可能会遇到批量向数据库中导入数据,如果批处理的情况较多的情况下可以使用spring batch,如果只是一个导入功能的话可以考虑使用jdbc的preparedStatement处理. ...

  6. Mac OS 安装robotframework

    1,查看当前系统默认的Python路径 which python1==> /usr/bin/python 2,查看当前python 版本 python1==> Python 2.7.10 ...

  7. 69.js--点击事件等比例弹出层div

    html:<!--弹出层导航栏--> <div class="public-nav-content"> <ul> <li><a ...

  8. SpringMVC和Struts2的区别及优势

    1.SpringMVC和Struts2的区别比较 1.Struts2是类级别的拦截, 一个类对应一个request上下文,SpringMVC是方法级别的拦截,一个方法对应一个request上下文,而方 ...

  9. Oauth2.0安全问题浅谈

    大家如果对Oauth还不是很了解可以先看下这篇文章https://www.cnblogs.com/maoxiaolv/p/5838680.html 我这篇博客主要是总结一下安全测试过程中遇到Oauth ...

  10. 北京大学Cousera学习笔记--2-计算导论与C语言基础-第一讲.计算机的基本原理-图灵机

    有限状态读写头从一个初始状态开始,对存储器上的输入数据进行读或写操作,经过有限步操作之后停机,此时存储器上的输出数据就是计算结果 (1) 图灵机的构成: 1.一条存储带:双向无限延长:上有一个个的小方 ...