《算法导论的Java实现》 11 基本数据结构

11 基本数据结构

code前要说的话
每次都是code后会有感想,这次却是code前有话要说。
“数据结构”是大学计算机里一个72课时的主干课程,教科书里的伪代码基本类似Pascal,也有用C来实现的。
而我现在要用Java来实现却遇到一点小问题。
首先,Java的基本API里面,各种数据结构非常完备,如果我只是抄袭一遍,将没有任何意义。然而,由于已经浏览过那些API,所以实现《算法导论》的数据结构伪代码时,不受Java底层API的影响,是不可能的。
其次,如果是C,那么很多都是指针操作,我反而不用去考虑指针指向的那个区域是什么类型(一般是个struct)。而Java这种面向对象的语言,到了现在又支持泛型。
我以前的排序算法的Java实现里,尽量都支持了泛型。但是现在,考虑到很多的数据结构的存储都是用数组,数组加泛型,我尽量去做,但是会不会遇到问题?我实在没有把握。

11.1 栈和队列
伪代码:
STACK-EMPTY(S)
1  if top[S] = 0
2      then return TRUE
3      else return FALSE

PUSH(S, x)
1  top[S] ← top[S] + 1
2  S[top[S]] ← x

POP(S)
1  if STACK-EMPTY(S)
2     then error "underflow"
3     else top[S] ← top[S] - 1
4          return S[top[S] + 1]



Java代码:

import java.util.Arrays;
import java.util.EmptyStackException;

public class Stack<E> {
    private int count;
    private Object[] objs;
    private int capacityIncrement;

    private int top() {
        return count;
    }

    private void ensureCapacityHelper(int minCapacity) {
        int oldCapacity = objs.length;
        if (minCapacity > oldCapacity) {
            int newCapacity = (capacityIncrement > 0) ? (oldCapacity + capacityIncrement)
                    : (oldCapacity * 2);
            if (newCapacity < minCapacity) {
                newCapacity = minCapacity;
            }
            objs = Arrays.copyOf(objs, newCapacity);
        }
    }

    public boolean isEmpty() {
        return top() == 0;
    }

    public synchronized E push(E item) {
        ensureCapacityHelper(count + 1);
        objs[count++] = item;
        return item;
    }

    @SuppressWarnings("unchecked")
    public synchronized E pop() {
        if (isEmpty())
            throw new EmptyStackException();
        else {
            Object obj = (E) objs[count - 1];
            count--;
            objs[count] = null;
            return (E) obj;
        }
    }

    public Stack() {
        this(10);
    }

    public Stack(int initialCapacity) {
        this(initialCapacity, 0);
    }

    public Stack(int initialCapacity, int capacityIncrement) {
        objs = new Object[initialCapacity];
    }

    public static void main(String[] args) {
        Stack<Integer> stack = new Stack<Integer>();
        for (int i = 0; i < 11; i++) {
            stack.push(i);
        }
        while (!stack.isEmpty()) {
            System.out.print(stack.pop() + " ");
        }
    }

}




输出:
10 9 8 7 6 5 4 3 2 1 0

code后感
用Java实现的时候,我以伪代码为基准,同时参照了java.util.Stack和java.util.Vector的源代码。
《算法导论》的原文里有一句“不考虑栈溢出的问题”,但是我的实现却不能不考虑。毕竟现在这些代码不仅仅是研究用的,大部分可以直接运用在实际项目中。
所以,ensureCapacityHelper这个函数被我原封不动的copy过来用。这个函数的作用就是,当栈上溢的时候,扩张栈的大小。类似的操作,在别的数据结构里还会出现。
在参考了Java底层的API之后,我们会发现:当栈上溢的时候,ensureCapacityHelper被调用,但是当pop被调用时,栈的大小不会被变小,虽然有这么一句“objs[count] = null;”,GC会释放栈元素占用的内存,但是栈本身占用的空间不会缩小。
所以在实际运用中,如果是使用Java的API的话,Stack一旦被用完,最好释放掉,不要重复利用。如果是自己动手实现Stack,最好在pop之后,有个timing,按照capacityIncrement,去减小数据区的大小。


伪代码:
ENQUEUE(Q, x)
1  Q[tail[Q]] ← x
2  if tail[Q] = length[Q]
3     then tail[Q] ← 1
4     else tail[Q] ← tail[Q] + 1

DEQUEUE(Q)
1  x ← Q[head[Q]]
2  if head[Q] = length[Q]
3     then head[Q] ← 1
4     else head[Q] ← head[Q] + 1
5  return x



Java代码:
import java.util.Arrays;
import java.util.NoSuchElementException;

public class Queue<E> {
    private int head = 0;
    private int tail = 0;
    private Object[] objs;
    private int capacityIncrement;

    public boolean isEmpty() {
        return head == tail;
    }

    public synchronized void enqueue(E item) {
        objs[tail] = item;
        if (tail == objs.length - 1)
            tail = 0;
        else
            tail++;
        if (tail == head) {
            int oldLength = objs.length;
            ensureCapacityHelper(objs.length + 1);
            for (int i = tail; i < oldLength; i++) {
                objs[i + objs.length - oldLength] = objs[i];
            }
            head += objs.length - oldLength;
        }
    }

    @SuppressWarnings("unchecked")
    public synchronized E dequeue() {
        if (isEmpty())
            throw new NoSuchElementException();
        Object obj = objs[head];
        if (head == objs.length - 1)
            head = 0;
        else
            head++;
        return (E) obj;
    }

    private void ensureCapacityHelper(int minCapacity) {
        int oldCapacity = objs.length;
        if (minCapacity > oldCapacity) {
            int newCapacity = (capacityIncrement > 0) ? (oldCapacity + capacityIncrement)
                    : (oldCapacity * 2);
            if (newCapacity < minCapacity) {
                newCapacity = minCapacity;
            }
            objs = Arrays.copyOf(objs, newCapacity);
        }
    }

    public Queue() {
        this(10);
    }

    public Queue(int initialCapacity) {
        this(initialCapacity, 0);
    }

    public Queue(int initialCapacity, int capacityIncrement) {
        objs = new Object[initialCapacity];
    }

    public static void main(String[] args) {
        Queue<Integer> queue = new Queue<Integer>();
        for (int i = 0; i < 8; i++)
            queue.enqueue(i);

        for (int i = 0; i < 4; i++)
            System.out.print(queue.dequeue() + " ");

        for (int i = 0; i < 9; i++)
            queue.enqueue(i + 10);

        while (!queue.isEmpty())
            System.out.print(queue.dequeue() + " ");

    }

}



输出:
0 1 2 3 4 5 6 7 10 11 12 13 14 15 16 17 18


code后感
简短的伪代码的Java实现如此冗长,原因在于要考虑溢出。
入队时,如果溢出,我也使用了ensureCapacityHelper来扩张objs数组,并且调整指针head,以及移动相应的数组内容。
出队和栈的pop一样,如果objs数组被扩张过,也不会恢复。所以,也一样可以考虑在某个timing缩小objs数组。



11.2 链表
LIST-SEARCH(L, k)
1  x ← head[L]
2  while x ≠ NIL and key[x] ≠ k
3      do x ← next[x]
4  return x

LIST-INSERT(L, x)
1  next[x] ← head[L]
2  if head[L] ≠ NIL
3     then prev[head[L]] ← x
4  head[L] ← x
5  prev[x] ← NIL

LIST-DELETE(L, x)
1  if prev[x] ≠ NIL
2      then next[prev[x]] ← next[x]
3      else head[L] ← next[x]
4  if next[x] ≠ NIL
5      then prev[next[x]] ← prev[x]




Java代码:
public class List<E> {

    private transient Entry<E> header = new Entry<E>(null);

    public static class Entry<E> {
        E element;
        Entry<E> next;
        Entry<E> previous;

        Entry(E element) {
            this.element = element;
        }
    }

    public Entry<E> search(E x) {
        if (x == null)
            return null;
        Entry<E> entry = header;
        while (entry != null && !x.equals(entry.element))
            entry = entry.next;
        return entry;
    }

    public Entry<E> insert(E x) {
        Entry<E> newEntry = new Entry<E>(x);
        newEntry.next = header;
        if (header != null)
            header.previous = newEntry;
        header = newEntry;
        newEntry.previous = null;
        return newEntry;
    }

    public void delete(E x) {
        Entry<E> entry = search(x);
        if (entry.previous != null)
            entry.previous.next = entry.next;
        else
            header = entry.next;
        if (entry.next != null)
            entry.next.previous = entry.previous;
    }

    public static void main(String[] args) {
        List<Integer> ll = new List<Integer>();
        ll.insert(1);
        ll.insert(2);
        ll.insert(3);
        ll.insert(4);
        ll.delete(3);
        Entry<Integer> e = ll.search(2);
        System.out.println(e.next.element);
        System.out.println(e.previous.element);
    }

}



输出:
1
4

code后感
我仿照了LinkedList的代码,定义了内部类Entry。因为Java没有指针,所以在一个实体上加类Entry类型的next和previous,来指向前一个实体和后一个实体。


哨兵(Sentinels)
LIST-DELET′ (L, x)
1  next[prev[x]] ← next[x]
2  prev[next[x]] ← prev[x]

LIST-SEARC′(L, k)
1  x ← next[nil[L]]
2  while x ≠ nil[L] and key[x] ≠ k
3      do x ← next[x]
4  return x

LIST-INSER′ (L, x)
1  next[x] ← next[nil[L]]
2  prev[next[nil[L]]] ← x
3  next[nil[L]] ← x
4  prev[x] ← nil[L]



Java代码:
忽略

code后感
我忽略了哨兵的链表的Java实现,却要写code后感,看上去有点滑稽。
实际上,前面非哨兵的链表Java程序里,header这个Entry在伪代码里是个函数,它恰好就可以充当哨兵nil[L],所以只要稍微改写一下前面的代码,就可以把一个双向链表变成一个双向环链表。除非后面的应用有需要环链表,否则我不打算实现这段伪代码了。



  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值