面试常备---栈和队列总结篇

      正式学习编程也就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语句来代替。
     栈的弹出和压入是栈最重要的两个基本动作,也是经常要被考察的知识点。

题目三:输入两个整数序列,第一个序列表示压栈顺序,判断第二个序列是否是弹出顺序。

     既然是两个整数序列,那么辅助栈是需要的。但是这里有个问题:压栈顺序在数字序列没有被压入栈的时候就已经确定了,根本不需要将整个数字序列完全压入栈后才知道压栈顺序,所以,真正涉及到压栈动作的是辅助栈。
     我们来看看一个简单的数字序列:{1,2,3,4},弹出顺序应该是{4,3,2,1}。在验证后面的序列是否是弹出顺序时,我们先看看数字1。既然1是第一个入栈的,那么在第二个序列中应该是最后一个元素,如果将第二个序列压入辅助栈,应该是栈顶元素,然后我们将它弹出去。这样一步一步的检查辅助栈每次栈顶的元素是否和第一个序列对应的数字相同,就能知道第二个序列是否是弹出序列。
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个部分:代码区,静态数据区和动态数据区,动态数据区就是我们常说的堆栈。栈和堆是两种不同的动态数据区,栈是一种线性结构,而堆是一种链式结构。进程中的每个线程都有自己的栈,因此即使每个线程的代码是一样的,但是它们的局部变量是互不干扰的。

      一个堆栈可以通过基地址和栈顶地址来描述。全局变量和静态变量分配在静态数据区,局部变量分配在堆栈中,所以我们可以通过堆栈的基地址和偏移量来访问局部变量。
      前面的讨论是基于C,如果是C++和Java,还有一种通过new来分配内存的方式,这时就是存储在堆中。
      栈和堆有什么区别呢?
      栈的内存空间由操作系统自动分配和释放,但是堆上的内存空间就只能手动分配和释放,所以我们在C++中经常是在new一个对象后,再显式的删除该对象。
      为什么要这样呢?因为栈的空间是有限的,有序的,所以操作系统可以方便的对它进行管理,但是堆是一个很大的自由存储区,无序的,要想正确的删除某个对象所占有的内存,系统可能需要花点时间来查找,这在new的对象足够多的情况下,是一个严重的效率问题,所以C++并不会帮我们自动处理,它将这个责任完全交给用户,而Java通过垃圾回收器在一定的程度上缓解了这个问题,注意,是缓解而不是解决。
       C中的malloc函数分配的内存空间就是在堆上,而程序在编译期对变量和函数分配内存都是在栈上,并且程序运行中函数调用参数的传递也是在栈上。

   

 

posted @ 2013-09-15 11:16  文酱  阅读(4749)  评论(1编辑  收藏  举报