算法分析学习笔记(二) - 栈和队列(下)

六.一些有关栈和队列的面试题(取自“Cracking The Coding Interview”一书)


1. How would you design a stack which, in addition to push and pop, also has a function min which returns the minimum element? Push, pop and min should all operate in O(1) time.
解答:题目的意思是,实现一个栈,不光支持基本栈操作,还要支持O(1)的min()操作。思路是,用两个堆栈来实现,一个栈正常使用,另一个栈负责存储最小值。另一个思路是在结点中多加一个min来存储当前的最小值。

2. Implement a MyQueue class which implements a queue using two stacks.
解答:用两个堆栈实现一个队列,思路是创建一个inStack和一个outStack,inStack负责入队列,outStack负责出队列,当outStack为空时,将inStack中内容压入outStack,完整实现请参考 https://github.com/leesper/algorithms_4ed/blob/master/project2/QueueWithTwoStacks.java

3. An animal shelter holds only dogs and cats, and operates ona strictly “first in, first out” basis. People must adopt either the “oldest”(based on arrival time) of all animals at the shelter, or they can selectwhether they could prefer a dog or a cat (and will receive the oldest animal ofthat type). They cannot select which specific animal they would like. Createthe data structures to maintain this system and implement operations such asenqueue, dequeueAny, dequeueDog and dequeueCat. You may use the built-in LinkedList data structure.
解答:在数据结构的内部创建两个链表,狗链表和猫链表,每个结点带一个order属性值来表示进队列的先后,order值越小表示进队列时间越久,以此来做区分实现dequeueAny。我的解答思路是另外一种,即dequeue出来发现不是想要的又塞回去,貌似效率没有标准答案的好,地址是 https://github.com/leesper/algorithms_4ed/blob/master/project2/AnimalQueue.java

七. 窥探数据结构的奥密——迭代器

     上面我们对栈和队列的讨论,还有一个关键问题没有讲到——迭代。比如我们不想取出任何元素,仅仅只是想遍历一遍数据结构以完成某个操作,比如遍历一遍队列,打印所有元素的值。如果我们要一个一个地出队列,打印然后再入队列,太繁琐还容易出错。这个时候容器类的设计者就需要提供 一种机制,在不暴露抽象数据类型内部实现细节的基础上让类的使用者能够迭代每个元素完成某种操作。迭代器(Iterator)模式就是用来解决这个问题的,在Java中,如果某个类实现了迭代器,那么类的使用者可以在完全不知道内部实现的情况下采取如图7-1和图7-2的方式来遍历该容器。

图7-1 迭代器的使用方式1

图7-2 迭代器的使用方式2
     在Java中,迭代器类也是作为私有内部类在容器类中实现的,简单点的迭代器主要提供next()和hasNext()操作,复杂的迭代器支持正向迭代和反向迭代,更复杂的迭代器还支持迭代过程中添加,删除和修改元素。对于C++来说,一个容器类往往要实现两种迭代器,一个常量迭代器一个非常量迭代器,还要进行适当的运算符重载。下面 这段代码就是上面队列迭代器的一种实现。
    private class ListIterator<Item> implements Iterator<Item> {
        private Node<Item> current;

        public ListIterator(Node<Item> first) {
            current = first;
        }

        public boolean hasNext()  { return current != null;                     }
        public void remove()      { throw new UnsupportedOperationException();  }

        public Item next() {
            if (!hasNext()) throw new NoSuchElementException();
            Item item = current.item;
            current = current.next; 
            return item;
        }
    }
     值得一提的是,如果我们为Go语言编写容器类,也有很多种实现迭代器的方式,比如编写一个迭代器的接口然后实现这些接口。其中最简单的方法是使用Channel加上Goroutine来实现,如下就是Go语言版本的栈实现,以及迭代器使用示例。
package cntr

import (
        "fmt"
)

type Stack struct {
        first   *node
        n               int
}

func NewStack() *Stack {
        return &Stack{}
}

func (stk *Stack) Push(item interface{}) {
        if item == nil {
                panic(fmt.Sprintf("pushing nil items"))
        }
        nd := newNode(item)
        oldfirst := stk.first
        stk.first = nd
        nd.next = oldfirst
        stk.n++
}

func (stk *Stack) Pop() interface{} {
        if stk.Empty() {
                panic(fmt.Sprintf("poping empty stack"))
        }
        item := stk.first.item
        stk.first = stk.first.next
        stk.n--
        return item
}

func (stk *Stack) Empty() bool {
        return stk.first == nil
}

func (stk *Stack) Size() int {
        return stk.n
}

func (stk *Stack) Iterator() <-chan interface{} {
        ch := make(chan interface{})
        go func() {
                for it := stk.first; it != nil; it = it.next {
                        ch <- it.item
                }
                close(ch)
        }()
        return ch
}
 
    
func TestStack(t *testing.T) {
        fmt.Println("========Testing For Stack========")
        stk := cntr.NewStack()
        stk.Push("My")
        stk.Push("Favourite")
        stk.Push("Programming Language")
        stk.Push("Is")
        stk.Push("Golang")

        for item := range stk.Iterator() {
                fmt.Println(item.(string))
        }

        fmt.Printf("stack size: %d\n", stk.Size())
        for !stk.Empty() {
                fmt.Println(stk.Pop())
        }
        fmt.Println(stk.Empty())
}

八.总结:实现抽象数据类型的两种方法

     综上所述,实现抽象数据类型主要有两种方式:链表和动态数组。链表实现方式的特点在于所有的基本操作在最坏情况下能够保证O(1)时间,不足之处在于需要付出额外的空间来维护指针;动态数组实现方式的特点在于浪费的空间较少,不足之处在于所有的基本操作具有摊销意义上的常量时间——某一时刻可能性能突然下降,但大多数情况下是好的。那么我们在解决具体问题时就需要根据应用场景进行取舍,如果对操作的实时性要求较高,则最好采用链表方式来实现,比如控制飞机起飞和降落的程序;如果要求不那么严格,而内存空间又比较紧张,就可以采用动态数组的方式来实现。

九.实际应用——实现双端队列与随机队列

     该问题的完整描述来自于 http://coursera.cs.princeton.edu/algs4/assignments/queues.html,要求是实现两个抽象数据类型——Deque和RandomizedQueue。Deque是对栈和队列的一种扩展,它支持从数据结构的头部和尾部插入和删除元素,要求所有的操作(包括迭代器)在最坏情况下的效率为O(1);RandomizedQueue是Queue的一种变种,它随机地出队列一个元素,要求所有的操作在摊销意义上的常量时间内完成,且迭代器实现是随机且相互独立的,它们的API如下所示。
public class Deque<Item> implements Iterable<Item> {
   public Deque()                           // construct an empty deque
   public boolean isEmpty()                 // is the deque empty?
   public int size()                        // return the number of items on the deque
   public void addFirst(Item item)          // insert the item at the front
   public void addLast(Item item)           // insert the item at the end
   public Item removeFirst()                // delete and return the item at the front
   public Item removeLast()                 // delete and return the item at the end
   public Iterator<Item> iterator()         // return an iterator over items in order from front to end
   public static void main(String[] args)   // unit testing
}
 
   
public class RandomizedQueue<Item> implements Iterable<Item> {
   public RandomizedQueue()                 // construct an empty randomized queue
   public boolean isEmpty()                 // is the queue empty?
   public int size()                        // return the number of items on the queue
   public void enqueue(Item item)           // add the item
   public Item dequeue()                    // delete and return a random item
   public Item sample()                     // return (but do not delete) a random item
   public Iterator<Item> iterator()         // return an independent iterator over items in random order
   public static void main(String[] args)   // unit testing
}
     思路,根据题目给出的条件,不难看出Deque的实现要采用链表方式,而RandomizedQueue的实现要采用动态数组。Deque的实现使用了两个指针first和last分别指向头部和尾部,因为执行removeLast()时需要知道尾结点前面的结点,所以结点除了next指针外还需要prev指针;RandomizedQueue是基于动态数组实现的,而出队列又要求随机,那么就要用StdRandom类产生一个均匀分布的在[0, N)之间的随机数,并与N-1交换,然后弹出a[N-1];而其迭代器也要求随机遍历元素,且不同迭代器之间是独立的,实现方式是取出数组下标构成一个index[]的下表数组,同样用StdRandom对index[]进行随机洗牌,然后按照产生的随机数组下标遍历容器中的元素。详细实现请参考 https://github.com/leesper/algorithms_4ed/blob/master/project2/Deque.javahttps://github.com/leesper/algorithms_4ed/blob/master/project2/RandomizedQueue.java

十. 干货——链表类面试题的解法总结

     因为链表是数据结构中的基石,技术面试时常常会围绕链表这个主题考察编程能力。链表类题目具有一定的技巧性,一道题往往会有多种不同的解法,常见的解题思路有四种:龟兔指针法,递归法,归并法和Partition法。

1. 龟兔指针法

     使用两个指针,一个龟指针走得慢,一次走一格;一个兔指针走得快,一次走两格或者提前走几步,通过同时迭代两个指针来解决问题。
1)问题举例:Implement an algorithm to find the kth to last element of a singly linked list.
思路:让龟指针站在第一个结点位置,兔指针先走k-1步,然后两个指针一起走,当兔指针走到链表最后一个结点时,龟指针指向的那个节点就是倒数第k个,详细的实现请参考 https://github.com/leesper/algorithms_4ed/blob/master/project2/SinglyLinkedList.java
2)问题举例:Given a circular linked list, impplement an algorithm which returns node at the beginning of the loop.
DEFINITION: Cicular Link list: A(corrupt) linked list in which a node's next pointer points to an earlier node, so as to make a loop in the linked list.
EXAMPLE: Input: A->B->C->D->E->C[the same C as earlier] Output: C
思路:非常经典的判断链表是否有环的问题,现在还多加了一个要求,要找出这个环的头部。方法仍然是龟兔指针法,但这次让兔指针一次走两步,龟指针一次走一步,这样若两个指针再次相遇,这个链表中肯定存在环,在龟兔指针相遇的结点上,保持龟指针不动,兔指针回到链表头,然后两个指针按照一次一个结点的速度同时走,再次相遇的那个结点就是环的头部。详细实现请参考 https://github.com/leesper/algorithms_4ed/blob/master/project2/FindCircular.java

2. 递归法:因为链表本身就是一个递归的结构,因此对于一些常规方法解起来困难的问题,也许用递归思维能简单高效地解决。

1)问题举例:判断链表是否是回文
思路:链表逆序排列后逐个比较(间接递归,因为链表的逆序可以写成递归算法)。或者直接递归:若链表头部和尾部相等,则只需要判断掐头去尾后的链表是不是回文即可。
2)链表逆序排列
思路:用递归思维,可以看成是将链表剩余部分逆序排列,然后再将头结点插入到尾部;第二种思路是从第二个结点开始遍历,每次都删除然后插入头部;第三种方法 最直接,用三个指针遍历链表,老老实实挨个逆序连接。

3. 归并法:来自于归并排序,思路是将两个链表按照某种规则拆分然后合并为一个链表。

1)问题举例:Write code to remove duplicates from an unsorted linked list, how would you solve this problem if a temporary buffer is not allowed ?
思路:如果题目允许用额外的空间,那么我们建立一个hash表,然后遍历链表,第一次遇到的在hash表中记录一下,第二次遇到的直接删除即可;若不允许用,则需要将链表转化为排序链表,这里可以用归并排序的思路,然后再删除重复元素。
2)问题举例:You have two numbers represented by a linked list, where each node contains a single digit. The digits are stored in reverse order, such that the 1’s digit is at the head of the list. Write a function that adds the two numbers and returns the sum as a linked list.
EXAMPLE
Input: (7 -> 1 -> 6), (5 -> 9 -> 2)
Output: 2 -> 1 -> 9
思路:归并,从两个链表的头部开始遍历,求和取得个位数上的数字创建新链表,取得十位上的数字作为进位,最后把较长链表剩余结点归并过来即可,参考 https://github.com/leesper/algorithms_4ed/blob/master/project2/DigitAdder.java

4. Partition法:来自于快速排序,是以某一个值为基准,将小于它的元素往前放,大于它的元素往后放,最后该值的左边全是比它小的元素,而右边全是比它大的元素。

1)问题举例:Write code to partition a linked list around a value x, such that all nodes less than x come before all nodes greater than or equal to x.
思路:用四个指针,beforeStart,beforeEnd,afterStart和afterEnd,将比x小的结点链入beforeXXX维护的链表,将比x大的结点链入afterXXX维护的链表,最后将afterXXX的链表并入beforeXXX的后面即可。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值