包,栈与队列

来源:互联网 发布:郑州知名软件企业排名 编辑:程序博客网 时间:2024/05/22 08:00

介绍

Algorithm part1总结篇第二篇,被讲烂了的包,栈,队列和感悟。
作为一个容器,它们有的基本操作有insert(加入),remove(删除),iterate(遍历),isEmpty(是否为空),也就如果要重写或自己写一个容器也要有这四种方法。
在其中包无序添加删除,不具体考虑。
栈操作最迟加入的元素,也就是后进先出(LIFO)
相对的队列操作最早加入的元素,也就是先进先出(FIFO)

栈(stack)

在说栈与队列之前,首先要搞清的应该是它们是数据结构,并不是内存之中的栈与堆,也不与最后实现方法的数组或链表挂钩。

  • 栈的链表实现

    这里写图片描述

private class Node{ String item; Node next;}

pop实现:

  1. 保存第一个节点的Item
  2. 删除第一个节点
  3. 返回Item

这里写图片描述

实现代码:

public String pop() { String item = first.item; first = first.next; return item; }

push实现:

  1. 维护原第一个节点的引用
  2. 创建一个新节点
  3. 将first引用指向新节点
  4. 将first的next引用指向原节点

这里写图片描述

实现代码:

public void push(String item) { Node oldfirst = first; first = new Node(); first.item = item; first.next = oldfirst; }
  • 栈的数组实现

这里写图片描述

代码实现:

public boolean isEmpty() { return N == 0; } public void push(String item) { s[N++] = item; } public String pop() { return s[--N]; }

这其中未提及就是数组实现比较重要的方法扩容(resize)来防止元素溢出,但其实用的很简单的实现方式,问题在于扩大和收缩的时机和倍数,Java之中的倍数非常科学,而自己实现只要满足需求即可,在下面讨论。
同时也需要抛出空指针异常防止下溢。
其次要防止内存泄漏,要将pop的节点在数组中删除。
于是pop方法改为:

public String pop(){ String item = s[--N]; s[N] = null; return item;}
  • resize
    扩容
    方案一:push()一次增加一个节点容量
    方案二:push()后检查容量,若节点满了,则将容量扩大到两倍。

方案一时间消耗:1 + 2 + … + N ~ N ^ 2 / 2
方案二时间消耗:N + (2 + 4 + 8 + … + N) ~ 3N.

但要注意是对于方案一来说,每次push()都会执行resize()方法,而对方案二来言,是阶段性执行resize()。

缩容
方案一:pop()一次减少一个节点容量
方案二:pop()后检查容量,若节点数为0,则将容量缩减到两分之一。
方案三:pop()后检查容量,若节点数为容量的二分之一,则将容量缩减到两分之一。
方案四:pop()后检查容量,若节点数为容量的四分之一,则将容量缩减到两分之一。

缩容与扩容不同,是纯粹为了节省内存,就算不做也不会有逻辑错误,所以为了有效地缩容,情况稍微复杂一些。
方案一与扩容情况相同,一样消耗太大不可取。
而方案二可能会很容易被想到,而它只会pop()操作过程中执行一次,和不优化几乎没有区别。
方案三是一个很难看出问题的方案,见图例:
这里写图片描述
可以看见数组在N=4与5之间时,数组容量反复变动,并没有达到理想效果,而且并非例子很极端,而是在节点数为容量的二分之一是缩容,就会让节点数接近容量,也是接近了满容,会不断在两个触发条件徘徊。
方案四解决了这个问题,在缩容和扩容后,节点数都为容量的二分之一这一最佳点,解决了这一问题。

最后分析一下内存消耗:

Implementation time linked list ~ 40 N array full ~ 8 N to one-quarter full ~ 32 N

链表的特点:
每次操作时间复杂度O(1),实现简洁且会少考虑很多安全问题,但会消耗额外的时间和内存去维护引用。
数组的特点:
每次平均操作时间复杂度O(1),更少的内存消耗,但会有例如扩容等一系列的问题。

队列(queue)

  • 队列的链表实现

这里写图片描述

dequeue实现:

  1. 保存第一个节点的Item
  2. 删除第一个节点
  3. 返回Item

这里写图片描述

代码实现:

public String dequeue() { String item = first.item; first = first.next; if (isEmpty()) last = null; return item; }

与pop方法基本相同,由此可见链表实现的步骤会很相似,不过要维护两个引用first和last,所以代码会有些不同。

enqueue实现:

  1. 维护原最后一个节点的引用
  2. 创建一个新节点
  3. 将last引用指向新节点
  4. 将原节点的next引用指向last

这里写图片描述

代码实现:

public void enqueue(String item) { Node oldlast = last; last = new Node(); last.item = item; last.next = null; if (isEmpty()) first = last; else oldlast.next = last; }

除了也要注意维护两个引用之外,要注意这个不是双向链表,所以要搞清指向关系。

  • 队列的数组实现

这里写图片描述

代码实现:

public boolean isEmpty() { return head == tail; } public void enqueue(String item) {    if(tail++ % capacity == head)       resize();   s[tail] = item;  } public String dequeue() {    if(tail - head == capacity / 4)       resize();   String item = s[head];    s[head] = null;   return item; }

由于是循环队列,所以这里要注意head和tail值,在此并未将head和tail值循环,控制在0~capacity之间,因此可以算出容量所以判断容器是否为空和是否要扩容和缩容比较简单。并且由于是数组,同样要在数组中将要dequeue的节点设为null。

应用作业

题目见
http://coursera.cs.princeton.edu/algs4/assignments/queues.html
栈和队列比较常用,会写一个变式也是常有,这次作业就是两个变式双向链表和随机队列,很容易看出来第一个用链表第二个用数组实现比较方便。
先说双向链表Deques,既然是双向,那在原来的Node中要多加一个引用pre来指向前一个节点并进行维护。其次API中对头尾都有加入删除操作,但并没有什么难度。最后由于是链表实现,没有办法相数组一样直接获得容量,遍历得到也太不划算,所以最好写一个全局变量count来记录容量,在加入删除成功时对应地加减count。
还有一点就是iterator和iterable的区别,在我看来,iterator要求容器提供的遍历的方法,而iterable要求该类提供提供遍历方法的容器。

部分代码实现:

    public void addFirst(Item item) {        // add the item to the front        if (item == null)            throw new NullPointerException();        if (isEmpty()) {            front = new Node();            front.item = item;            end = front;        } else {            Node oldFront = front;            front = new Node();            front.item = item;            front.next = oldFront;            oldFront.pre = front;        }        count++;        assert checkf();    }    public void addLast(Item item) {        // add the item to the end        if (item == null)            throw new NullPointerException();        if (isEmpty()) {            end = new Node();            end.item = item;            front = end;        } else {            Node oldEnd = end;            end = new Node();            end.item = item;            end.pre = oldEnd;            oldEnd.next = end;        }        count++;        assert checkf();    }    public Item removeFirst() {        // remove and return the item from the front        if (isEmpty())            throw new NoSuchElementException();        Item item = front.item;        front = front.next;        if (front != null)            front.pre = null;        count--;        if (isEmpty()) {            front = null;            end = null;        }        assert checkf();        return item;    }    public Item removeLast() {        // remove and return the item from the end        if (isEmpty())            throw new NoSuchElementException();        Item item = end.item;        end = end.pre;        if (end != null)            end.next = null;        count--;        if (isEmpty()) {            front = null;            end = null;        }        assert checkf();        return item;    }

在这其中的checkf主要测试了边界情况,包括count=1,2,3或n时情况。count=1,2,3时主要是first和last引用的情况,而为n时只要测试count与实际遍历到的节点数是否对应,这个测试很个人但却很好用。

再是随机队列,这个看了很多实现,感觉都大胆的不可相信,对很多情况并没有做考虑,这里简述一下:
首先这个队列删除节点是随机的,那么只用0~capacity间的随机数能真的随机删除节点吗。我觉得并不行,并且尝试了一下,用了其中的库函数发现并非每个数只出现一次,那么对已经是null的节点反复删除必定不合理,也达不到要求的随机,所以要判断这个位置的节点是否为null才是健壮的。
其次随机删除节点在数组中会有不间断的null出现,比原来的数组实现队列要复杂,在要扩容时是否要将null原封不动复制到新数组,还是略过null,同时在什么情况下才会扩容也成为了问题,因为在count==capacity之前可能head与tail就会产生冲突,这都是随机删除的一系列后果。
前一个问题我选择略过null,因为这类结点没有价值,而后一个问题我选择改变扩容的时机,在tail等于容量时就扩容,在复制数组时将capacity=2*count,使得新数组容量为节点数两倍且中间没有null。
最后是随机遍历,同样首先略过null,其次将新数组洗牌排序再顺序输出会更有效率。
最后仍旧有个附加内容,由于是个数组实现,貌似有最大长度,通不过最后数据结构最大长度的测试,对此我好像也束手无策。

部分代码实现:

    public void enqueue(Item item) {        // add the item        if (item == null)            throw new NullPointerException();        if (end >= capacity)            resize();        a[end] = item;        end++;        count++;        assert check();    }    private void resize() {        capacity = 2;        while (capacity / 2 < count) {            capacity *= 2;        }        Item[] old = a;        a = (Item[]) new Object[capacity];        end = 0;        count = 0;        for (Item i : old) {            if (i != null) {                a[end] = i;                end++;                count++;            }        }        old = null;    }    public Item dequeue() {        // remove and return a random item        if (isEmpty())            throw new NoSuchElementException();        if (count <= capacity / 4)            resize();        int random = StdRandom.uniform(end);        while (a[random] == null) {            random = StdRandom.uniform(end);        }        Item r = a[random];        a[random] = null;        count--;        assert check();        return r;    }
    public RandomInterator(Item[] old) {            t = (Item[]) new Object[count];            for (Item i : old) {                if (i != null) {                    t[rear] = i;                    rear++;                }            }            StdRandom.shuffle(t);        }        @Override        public boolean hasNext() {            return rear != 0;        }        @Override        public Item next() {            if (!hasNext())                throw new NoSuchElementException();            // int i = StdRandom.uniform(rear);            Item r = t[--rear];            // Item r = t[i];            // Item temp = t[i];            // t[i] = t[rear - 1];            // t[rear - 1] = temp;            // rear--;            return r;        }

continuing…

0 0
原创粉丝点击