数据结构与算法分析(Java语言描述)—— 表

一 Java Collections API 中的表

1.1 Coolection 接口
public interface Coolection<T> extends Iterable<T>{
    int size();
    boolean isEmpty();
    void clear();
    boolean contains(T t);
    boolean add(T t);
    boolean remove(Object o);
    java.util.Iterator<T> iterator();
}

Collection 接口扩展了 Iterable 接口,实现 Iterable 接口的那些类可以拥有增强的 For 循环。

public static <T> void print(Collection<T> coll){
    for(T item : coll){
        System.out.println(item);
    }
}
1.2 Iterator 接口
public interface Iterator<T>{
    boolean hasNext();
    T next();
    void remove();
}

实现 Iterable 接口的集合必须提供一个称为 iterator() 的方法,该方法返回一个 Iterator 类型的对象。

Iterator 接口的思路是,通过 iterator 方法,每个集合均可创建并返回给客户一个实现 Iterator 接口的对象,并将当前位置的概念在对象内部存储下来

当编译器见到一个正在用于 Iterable 的对象的增强 for 循环的时候,它调用 Iterator 的那些方法代替增强 for 循环,以得到一个 Iterator 对象,然后调用 next() 、hasNext(),因此,前面看到的 print() 例程由编译器重写。

public static <T> void print(Collection<T> coll){
    Iterator<T> it = coll.iterator();
    while( it.hasNext() ){
        T item = it.next();
        System.out.println(item);
    }
}

1.2.1 remove() 方法的用处和优点
a .Collection 的 remove 方法必须首先找出要被删除的项,如果知道所要删除的项的精确位置,那么删除它的开销很可能要小很多。
b. 当直接使用 Iterator (而不是通过增强的 for 循环间接使用)时,要记住一个重要的法则:如果对正在被迭代的集合进行结构上的改变(即对该集合使用add、remove 或 clear 方法),那么迭代器就不再合法(并且在其后使用该迭代器时,会有 Concurrent-ModificationException 异常被抛出)。然而,如果迭代器调用了它自己的 remove() 方法,那么这个迭代器就仍然是合法的。这是有时候我们更愿意用迭代器的 remove 方法的第二个原因。

1.3 List 接口、ArrayList 类和 LinkedList 类

List 接口中一些重要的方法

public interface<E> List extends Collection<E>{
    E get(int idx);
    E set(int idc);
    void add(int idx, E t);
    void remove(int idx);
    ListIterator<E> listIterator();
    ListIterator<E> listIterator(int pos);
}

List ADT 有两种流行的实现方式:

a. ArrayList 类提供了 List ADT 的一种可增长数组的实现。使用 LinkedList 的优点在于,对 get 和 set 的调用都花费常数时间。其缺点是新项的插入和现有项的删除代价昂贵,除非变动是在 ArrayList 的末端进行。
b. LinkedList 类则提供了 List ADT 的双链表实现。使用 LinkedList 的优点在于,新项的插入和现有项的删除均开销很小,这里假设变动项位置是已知的。使用 LinkedList 的缺点是它不容易作索引,因此对 get 的调用是昂贵的,除非调用非常接近表的端点(如果对 get 的调用是对接近表后部的项进行,那么搜索的进行可以从表的后部开始)。
这意味着,在表的前端进行添加和删除都是常数时间的操作,由此 LinkedList 更提供了 addFirst 和 removeFirst、addLast 和 removeLast,以及 getFirst 和 getLast 等以有效地添加、删除和昂文表两端的项

为了看出差别,我们考察对一个 List 进行操作的某些方法。首先,设我们通过在末端添加一些项来构造一个 List。

public static void makeList(List<Integer> lst, int N){
    lst.clear();
    for(int i = 0; i < N; i++){
        lst.add(i);
    }
}

不管 ArrayList 还是 LinkedList 作为参数被传递,makeList 的运行时间都是 O(N),因为对 add 的每次调用都是在表的末端进行,从而均花费常数时间(可以忽略对 ArrayList 偶尔进行的扩展)。另一方面,如果我们通过在表的前端添加一些项来构造一个 List;

public static void makeList(List<Integer> lst, int N){
    lst.clear();
    for(int i = 0; i < N; i++){
        lst.add(0, i);
    }
}

那么,对于 LinkedList 它的运行时间是O(N),但是对于 ArrayList 其运行时间则是O(N的二次方),因为在 ArrayList 中,在前端进行添加是一个 O(N) 操作。

扫描二维码关注公众号,回复: 1750519 查看本文章

下面一个例程时计算 List 中的数的和

public static int sum(List<Integer> lst){
    int total = 0;
    for(int i = 0; i < N; i++)
        total += lst.get(i);
    return total;
}

这里,ArrayList 的运行时间是 O(N),但对于 LinkedList 来说,其运行时间则是 O(N的二次方),因为在 LinkedList 中,对 get 的调用是 O(N) 操作。可是,要是使用一个增强的 for 循环,那么他对任意的一个 List 的运行时间都是 O(N),因为迭代器有效地从一项到下一项推进。
对搜索而言,ArrayList 和 LinkedList 都是低效的,对 collection 的 contains 和 remove 两个方法(他们都以 E 为参数)的调用均花费线性时间。
在 ArrayList 中有一个容量的概念,他表示基础数组的大小。在需要的时候,ArrayList 将自动增加其容量以保证它至少具有表的大小。如果该大小的早期估计存在,那么 ensureCapacity 可以设置容量为一个足够大的量,以避免数组容量以后的扩展。再有,trimToSize 可以在所有的 ArrayList 添加完成之后使用,以避免浪费空间。

1.4 例子:remove 方法对 LinkedList 类的使用

需求:
将一个表中的所有偶数的项删除。比如,如果表中包含6,5,1,4,2,则在该方法调用之后,表中仅有元素5,1。
思考:
是否可以建一个新表,然后拷贝奇数项?这样太浪费空间,我们写一个干净的避免拷贝的表,遇到偶数项时将它从表中删除。对于 ArrayList 这种几乎是在任意位置删除的行为是极其昂贵的代价,不过 LinkedList 给我们提供了可能。

第一种方案

public static void removeEvensVer1(List<Integer> lst){
    int i = 0;
    while( i < lst.size()){
        if( lst.get(i) % 2 == 0)
            lst.remove(i);
    }
}

分析:
在一个 ArrayList 上,我们知道,remove 的效率不是很高的,因此该程序花费的是二次时间。LinkedList 暴露两个问题。首先,对 get 的效率不高,因此例程花费的是二次时间。而且,对 remove 的调用也同样低效,因为达到位置 i 的代价是昂贵的。

第二种方案

public static void removeEvensVer2(List<Integer> lst){
    for( Integer x : lst ){
        if( x % 2 == 0)
            lst.remove(x);
    }
}

分析:
矫正了第一种方案,不是用 get ,而是用迭代器一步步遍历该表,这是高效率的。但是使用 Collection 的 remove 方法是非常低效的,因为 remove 方法必须再次搜索此项,花费线性时间。而且当一项被删除时,list 的结构发生变化,由增强 for 循环使用的基础迭代器是非法的,会报出异常。

第三种方案

public static void removeEvensVer3(List<Integer> lst){
    Iterator<Integer> it = lst.iterator();
    while( it.hasNext() ){
        if( it.next() % 2 == 0 )
            it.remove();
    }
}

分析:
一种成功的想法,使用迭代器的方式,使用迭代器的 remove 方法。对于一个 LinkedList 来说,对该迭代器的 remove 方法的调用只花费常数时间,而不是二次时间。对于 ArrayList 来说,即便迭代器位于被删除的节点上,其 remove 的代价依然很昂贵,整个程序仍然花费二次时间。

1.5 关于 ListIterator 接口

List 的 ListIterator 扩炸了 List 的 Iterator 的功能。方法 previous 和 hasPrevious 使得对表从后往前遍历得以完成。add 方法将一个新的项放在当前位置。对于 LinkedList 来说,add 时一种常数时间的操作,但对于 ArrayList 来说代价昂贵。set 改变迭代器看到的最后一个值,从而对 LinkedList 很方便。例如,他可以用来对 List 的所有偶数中减去1,而这对于 LinkedList 来说,不用 ListIterator 的 set 方法是很难做到的。

public interface ListIterator<E> extends Iterator<E>{
    boolean hasPrevious();
    boolean previous();

    void add(E e);
    void set(E newVal);
}

二 ArrayList 类的实现

建立一个 MyArrayList 类,实现细节:
1. MyArrayList 将保持基础数组,数组的容量,以及存储在 MyArrayList 中的当前项数。
2. MyArrayList 将提供一种机制以改变基础数组的容量。通过获得一个新数组,将老数组拷贝到新数组中来改变数组的容量。允许虚拟机回收老数组。
3. MyArrayList 将提供 get 和 set 的实现。
4. MyArrayList 将提供基本的例程,如 size、isEmpty、和 clear,它们是典型的单行程序;还提供 remove ,以及两种不同版本的 add,如果数组的大小和容量相同,那么这两个 add 增加容量。
5. MyArrayList 将提供一个实现 Iterator 接口的类。这个类将存储迭代序列中的下一项的下标,并提供 next、hasNext 和 remove 等方法的实现。MyArrayList 的迭代器方法直接返回实现 Iterator 接口的该类的新构造的实例。

2.1 基本类

为了把精力集中在编写迭代器类的基本方面,我们不检测可能使得迭代器无效的结构上的修改,也不检测非法的迭代器 remove 方法。

import java.util.Iterator;  
/** 
 * 自定义实现链表问题: 
 * 1.何时使用theSize,何时使用size(); 
 * 2.为何要扩容至两倍; 
 * 
 * @param <AnyType> 
 */  
public class MyArrayList <AnyType>implements Iterable<AnyType>  
{  
    /** 
     * 自定义链表的默认容量    10 
     */  
    private static final int DEFAULT_CAPACITY = 10;  
    /** 
     * 链表的大小——当前链表中的 元素的个数 
     */  
    private int theSize;  
    /** 
     * 用于链表实现的数组   
     */  
    private  AnyType [] theItems;  
    /** 
     * 构造方法 
     */  
    public MyArrayList()  
    {  
        doClear();  
    }  
    /** 
     * 清空链表 
     */  
    public void clear()  
    {  
        doClear();  
    }  
    /** 
     * 清空链表 
     */  
    private void doClear()  
    {  
        //设置链表元素个数为 0   
        theSize = 0;  
        //链表初始化方法,确保不发生异常  
        ensureCapacity(DEFAULT_CAPACITY);  
    }  
    /** 
     * 返回当前链表的元素个数 
     * @return 
     */  
    public int size()  
    {  
        return theSize;  
    }  
    /** 
     * 返回当前链表是不是空表(元素个数是否为0) 
     * @return 
     */  
    public boolean isEmpty()  
    {  
        return theSize == 0;  
    }  
    /** 
     * 将链表数组大小修改为元素个数大小,以避免空间资源浪费 
     */  
    public void trimToSize()  
    {  
        ensureCapacity(size());  
    }  
    /** 
     * 获取某个指定索引位置上的元素 
     * @param idx 索引参数 
     * @return 
     */  
    public AnyType get(int idx)  
    {  
        //如果索引参数小于0或者大于链表元素最大索引(size()-1,从零开始,等于链表元素个数-1);则抛出数组越界异常  
        if(idx<0||idx>=size())  
        {  
            throw new ArrayIndexOutOfBoundsException();  
        }  
        return theItems[idx];  
    }  
    /** 
     * 修改链表中指定索引位置处的元素的值为指定的值 
     * @param idx 索引参数 
     * @param newVal 新的元素值 
     * @return 原来的元素值 
     */  
    public AnyType set(int idx,AnyType newVal)  
    {  
        if(idx<0||idx>=size())  
        {  
            throw new ArrayIndexOutOfBoundsException();  
        }  
        AnyType old = theItems[idx];  
        theItems [idx] = newVal;  
        return old;  
    }  
    /** 
     * 对链表容量进行扩充,以保证不会发生数组越界。 
     * @param newCapacity 扩充后的容量 
     */  
    public void ensureCapacity(int newCapacity)  
    {  
        //如果需要扩增的容量小于当前链表的元素个数,则表明预期链表容量完全够用,不用扩容,直接返回。  
        if(newCapacity<theSize)  
        {  
            return;  
        }  
        //否则,就需要对现在的链表扩容至新容量大小,以保证后续操作不会使数组越界。  
        AnyType [] old = theItems;  
        theItems = (AnyType[]) new Object[newCapacity];  
        for(int i =0;i<size();i++)  
        {  
            theItems[i] = old[i];  
        }  
    }  
    /** 
     * 在指定索引位置新增元素 
     * @param idx 索引参数 
     * @param x 新增元素 
     */  
    public void add(int idx,AnyType x)  
    {  
        //如果链表数组还剩下一个元素的空位,则将链表容量扩容至两倍  
        if(theItems.length==size())  
        {  
               //+1是考虑 0 的时候的情况
            ensureCapacity(size()*2+1);  
        }  
        //从索引位置开始,其后每个元素都向后移动一个位置  
        for(int i = theSize;i>idx;i--)  
        {  
            theItems[i]=theItems[i-1];  
        }  
        //设置 位置索引为 idx处的元素的值为x  
        theItems[idx] = x;  
        //链表大小(元素个数)+1  
        theSize++;  
    }  
    /** 
     * 向链表最后端添加元素 
     * @param x 添加的新元素 
     * @return 是否添加成功 
     */  
    public boolean add(AnyType x)  
    {  
        add(size(),x);  
        return true;  
    }  
    /** 
     * 删除指定索引处的元素 
     * @param idx 位置索引参数 
     * @return 被删除的元素值 
     */  
    public AnyType remove(int idx)  
    {  
        if(idx<0||idx>=size())  
        {  
            throw new ArrayIndexOutOfBoundsException();  
        }  
        AnyType removedItem = theItems[idx];  
        for(int i=idx;i<size()-1;i++)  
        {  
            theItems[i]=theItems[i+1];  
        }  
        theSize --;  
        return removedItem;  
    }  
    /** 
     * 链表 迭代器 返回自定义迭代器 
     */  
    public Iterator<AnyType> iterator()   
    {  
        return new ArrayListIterator();  
    }  
    /** 
     * 内部类 实现Iterator 
     * 
     */  
    private class ArrayListIterator implements java.util.Iterator<AnyType>  
    {  
        /** 
         * 迭代器当前指针位置——与链表数组的索引对应 
         */  
        private int current = 0;  


        /** 
         * 是否有下一个元素 
         */  
        @Override  
        public boolean hasNext()  
        {  
            return current < size();  
        }  
        /** 
         * 返回下一个元素(当前索引+1 处) 
         */  

        public AnyType next()   
        {  
            if(!hasNext())  
            {  
                throw new java.util.NoSuchElementException();  
            }  
            return theItems[current++];  
        }  
        /** 
         * 迭代器 删除链表元素;循环中直接使用链表方法进行元素删除会抛出异常,需要使用迭代器的删除方法 
         * 问题:迭代器删除方法同样是调用链表删除元素的方法,为什么使用迭代器删除方法就不会报错,而使用链表删除方法就会报错? 
         */  
        public void remove()  
        {  
            MyArrayList.this.remove(--current);  
        }  
    }  
}  

三 LinkedList 类的实现

实现细节:
1. MyLinkedList 类本身,它包含到两端的链、表的大小以及一些方法。
2. Node 类,他可能是一个私有的嵌套类。一个节点包含数据以及到前一个节点的链和到下一个节点的链,还有一些适当的构造方法。
3. LinkedListIterator 类,该类抽象了位置的概念,是一个私有类,并实现接口 Iterator。它提供了方法 next、hasNext 和 remove 的实现。

import java.util.ConcurrentModificationException;
import java.util.Iterator;
import java.util.NoSuchElementException;

public class MyLinkedList<T> implements Iterable<T> {
    private int theSize;
    //modCount 代表自从构造以来对链表所做的改变次数。
    private int modCount = 0;
    private Node<T> beginMarker;
    private Node<T> endMarker;
    private static class Node<T>{
        public T data;
        public Node<T> prev;
        public Node<T> next;
        public Node(T e, Node<T> p, Node<T> n){
            data = e;
            prev = p;
            next = n;
        }
    }

    public MyLinkedList(){
        doClear();
    }
    public void clear(){
        doClear();
    }
    private void doClear() {
        beginMarker = new Node<T>(null, null, null);
        endMarker = new Node<T>(null, null, null);
        beginMarker.next = endMarker;

        theSize = 0;
        modCount++;
    }
    public  int size(){
        return  theSize;
    }
    public boolean isEmpty(){
        return theSize == 0;
    }
    public boolean add(T t){
        add( size(), t);
        return true;
    }
    public void add(int idx, T t){
        addBefore(getNode(idx, 0, size()), t);
    }
    public T get(int idx){
        return getNode(idx).data;
    }
    public T set(int idx, T newVal){
        Node<T> p = getNode(idx);
        T oldVal = p.data;
        p.data = newVal;
        return oldVal;
    }
    public T remove(int idx){
        return remove(getNode(idx));
    }
    private void addBefore(Node<T> p, T t){
        Node<T> newNode = new Node<>(t, p.prev, p);
        newNode.prev.next = newNode;
        p.prev = newNode;
        theSize++;
        modCount++;
    }
    private T remove(Node<T> p){
        p.next.prev = p.prev;
        p.prev.next = p.next;
        theSize++;
        modCount++;

        return p.data;
    }
    private Node<T> getNode(int idx){
        return getNode(idx, 0, size()-1);
    }
    private Node<T> getNode(int idx, int lower, int upper){
        Node<T> p;

        if(idx < lower || idx > upper)
            throw new IndexOutOfBoundsException();
        if(idx < size()/2){
            p = beginMarker.next;
            for(int i = 0; i < idx; i++)
                p = p.next;
        }
        else{
            p = endMarker;
            for (int i = size(); i > idx; i--)
                p = p.prev;
        }
        return p;
    }

    public Iterator<T> iterator() {
        return new LinkedListIterator();
    }

    private class LinkedListIterator implements Iterator<T>{
        private Node<T> current = beginMarker.next;
        private int expectedModCount = modCount;
        private boolean okToRemove = false;

        public boolean hasNext(){
            return current != endMarker;
        }
        public T next(){
            if(modCount != expectedModCount)
                throw new ConcurrentModificationException();
            if(!hasNext())
                throw new NoSuchElementException();
            T nextItem = current.data;
            current = current.next;
            okToRemove = true;
            return nextItem;
        }
        public void remove(){
            if(modCount != expectedModCount)
                throw new ConcurrentModificationException();
            if(!hasNext())
                throw new NoSuchElementException();

            MyLinkedList.this.remove(current.prev);
            expectedModCount++;
            okToRemove = false;
        }
    }
}

猜你喜欢

转载自blog.csdn.net/baidu_37181928/article/details/80731350