-栈和队列总结篇
正式学习编程也就1年而已,在这1年里,要学习C/C++,Java,C#这些主流语言,还要熟悉JavaScript,HTML,CSS这些前端开发知识,加上一些Android应用软件,网站站点的开发工作,导致我现在就是一个大杂烩,什么都知道一点,但又什么都不精通。现在又面临毕业找工作压力,不知道自己应该找什么工作,毕竟自己好像什么都碰过,心浮气躁,原本基础就是薄弱,还要在这段日子顶着压力,将手头上的项目努力完成,毕竟开发软件不难,但维护软件特别难,像是已经发布的网站,现在面临服务器被攻击而无法正常运行的问题。果然还是那句行内的老话:当软件正式上线运行的时候,真正麻烦的事情才正式开始啊!!
相信这也是现在应届毕业生的现状,困扰着是要准备笔试和面试,还是利用这短短的一个学期努力做出作品来。当然,两者都可以兼顾,可惜我们都不是那种游刃有余的人,尤其是像我们这样临时过来的码农临时工。。。
我学习的第一门语言是Java,虽然不精通,但算得上熟悉,无奈现在大部分的笔试和面试都是考察C/C++,尤其是指针,毕竟像是数据结构这些东西,很多都是用指针实现的。指针是我们学习C/C++的鬼门关,我刚从Java过来的时候,一看到指针就头疼,现在依然头疼,即使知道一些指针的高级技巧,像是一些看似非常强大的一句多重指针的代码浓缩好几行语句这类的东西,我是不支持的,也不认为自己能写好,想起C#的编程宗旨:能够朗读的代码,我就对指针有一种天生的恐惧:自己是否用错了指针,该指针是否空指针,所指向的内存是否被释放掉。。。在看一些C/C++的源码时,我就经常为那些变量名所困扰:大量的宏定义导致我无法从变量名推敲出它的真正意思!经常需要翻阅头文件查找相关的宏定义才能知道这个变量和类型是什么,也对那些意义不明的英文单词缩写的函数名感到头疼,基本函数库,像是字符串库还好,缩写还是很到位的,但一些第三方库就不敢恭维了。我情愿函数名写得长一点,也不愿为了所谓的短小写出不精悍的函数名出来。
我是一个学艺不精的码农,自认自己没有天赋,包括努力的天赋,可悲的是,努力也是需要天赋的,有些人就是非常懂得努力,他们能够迅速的掌握努力的技巧并找到持续努力的动力,这些人大部分都是以兴趣为起点,但像是我们这类的平庸码农,可能就是为了一碗饭碗。。。
不管怎样,我们都已经开始启动了,是慢跑,还是快跑,都已经不重要,最重要的是,能否坚持到终点,像是一些人,跑得很快,但也很快就没有体力倒下了,一些人,就算是慢跑,也是跑着跑着就没了,还在跑的,有哭的,也有笑的,更多的是像我们一样在死磕着。。。
毕竟,人生其实就是一场怎么也看不到终点的无限期马拉松,对个人而言,只有死亡才能让我们从这场比赛中脱身,但有谁知道死后是否还要在另一个世界中继续跑呢?
言归正传,栈和队列也是非常常见的数据结构,它们本身的特点就非常适合用来解决一些实际的问题。
栈对于学习计算机的人来说,是再熟悉不过的东西了,很多东西都需要用栈来存储,像是操作系统就会给每个线程创建一个栈用来存储函数调用时各个函数的参数,返回地址及临时变量等,函数本身也有一个函数栈用来存储函数的局部变量等。
栈的特点就是后进先出,需要O(N)的时间才能找到栈中的最值。
队列和栈刚好相反,是先进先出,表面上和栈是一对矛盾体,但实际上,都可以利用对方来实现自己。
题目一:用两个栈实现一个队列,并分别实现在队列尾部插入结点和在头部删除结点的功能。
template <typename T> class CQueue { public: CQueue(void); ~CQueue(void); void appendTail(const T& node); T deleteHead(); private: stack<T> stack1; stack<T> stack2; }; template<typename T> void CQueue<T> :: appendTail(const T& element) { stack1.push(element); } template<typename T> T CQueue<T> :: deleteHead() { if(stack2.size() <= 0) { while(stack1.size() > 0) { T& data = stack1.top(); stack1.pop(); stack2.push(data); } } if(stack2.size() == 0) { throw new exception("queue is empty"); } T head = stack2.top(); stack2.pop(); return head; }
题目二:定义栈的数据结构,在该类型中实现一个能够得到栈的最小元素的min函数,在该栈中,要求调用min,push和pop的时间复杂度都是O(1)。
template <typename T> void StackWithMin<T> :: push(const T& value) { m_data.push(value); if(m_min.size() == 0 || value < m_min.top()) { m_min.push(value); } else { m_min.push(m_min.top()); } } template <typename T> void StackWithMin<T> ::pop() { assert(m_data.size() > 0 && m_min.size() > 0); m_data.pop(); m_min.pop(); } template <typename T> const T& StackWithMin<T> :: min() const { assert(m_data.size() > 0 && m_min.size() > 0) return m_min.top(); }
其中,m_data是数据栈,而m_min就是辅助栈,而assert函数就是断言。所谓的断言,就是我们会提出一种假设,像是这样,就假设数据栈和辅助栈的大小都是大于0,这是用于测试使用,当然,我们也可以使用一般的if语句来代替。
栈的弹出和压入是栈最重要的两个基本动作,也是经常要被考察的知识点。
题目三:输入两个整数序列,第一个序列表示压栈顺序,判断第二个序列是否是弹出顺序。
bool IsPopOrder(const int* pPush, const int* pPop, int nLength) { bool bPossible = false; if(pPush != NULL && pPop != NULL && nLength > 0) { const int* pNextPush = pPush; const int* pNextPop = pPop; std :: stack<int> stackData; while(pNextPop - pPop < nLength) { while(stackData.empty() || stackData.top() != *pNextPop) { if(pNextPush - pPush == nLength) { break; } stackData.push(*pNextPush); pNextPush++; } if(stackData.top() != *pNextPop) { break; } stackData.pop(); pNextPop++' } if(stackData.empty() && pNextPop - pPop == nLength) { bPossible = true; } } return bPossible; }
考察栈并不一定考察我们是否会编写关于栈的代码,由于栈和计算机的内存存储方式有关,所以也会有关于这些的基本知识。
在计算机为一个程序段分配内存的时候,全局变量和静态变量分配的内存是连续的,并且是存放在数据段中。对于一个进程的内存空间而言,可以在逻辑上分为3个部分:代码区,静态数据区和动态数据区,动态数据区就是我们常说的堆栈。栈和堆是两种不同的动态数据区,栈是一种线性结构,而堆是一种链式结构。进程中的每个线程都有自己的栈,因此即使每个线程的代码是一样的,但是它们的局部变量是互不干扰的。