1. 程式人生 > 實用技巧 >實現ArrayList與LinkedList

實現ArrayList與LinkedList

前言

在平時的開發過程中我們會使用到許多的資料結構,其中表也許是使用最多的一種。明白Collections容器的朋友一定都是使用過其中的List容器。這裡我將通過建立自己的List來說明表的原始碼實現。

這裡主要實現兩個庫類重要子集ArrayListLinkedList的程式碼。

區別

ArrayListLinkedList分別是表的兩種實現方式。各自有著各自的優點和缺點。我們都知道ArrayList的底層是基於陣列的方式實現,而LinkedList則是基於連結串列的方式。

ArrayList

ArrayList是通過陣列來實現的一種資料結構,它由固定容量建立的一個數組,但是在需要的時候可以用雙倍容量建立一個不同的陣列。

優點:當需要對其中的元素進行索引時只需要花費常數時間。

缺點:當需要新增和刪除元素時最壞情況下的時間複雜度為O(N)。這是由於在新增或者刪除時需要移動陣列後面的所有元素。但是,如果需要新增或是刪除操作的元素位於表的高階時,則只花費O(1)的時間。

補充:當我們需要對一個數組進行擴容時,這將會是一件十分複雜的事情。

int[] oldArr = new int[10];
int[] newArr = new int[oldArr.length * 2];   //擴充套件後的陣列
for(int i=0;i<oldArr.length;i++){
    newArr[i] = oldArr[i];
}
oldArr = newArr;
LinkedList

LinkedList是由一系列結點組成,這些結點在記憶體中不需要連續,每一個結點都包含一個表元素(資料),以及到該元素後面一個結點的鏈(稱為next鏈)。最後一個結點的next鏈引用為null。

但是通常我們會使用到雙鏈表。雙鏈表就是在簡單的連結串列上給每個結點加上一個到它前面一個結點的。

優點:當需要進行新增或是刪除操作時只需要花費線性時間。當我們需要新增或是刪除節點時,只需要修改該節點的前後節點鏈即可。

缺點:當需要進行索引時最壞情況需要花費O(N)的時間複雜度。因為它需要遍歷整個連結串列。

Collections介面

Collections API位於java.util

包中。其中Collections介面擴充套件了Iterable介面,實現了Iterable介面的類可以擁有增強的for迴圈。

public interface Collection<T> extends Iterable<T>{
        int size();
        boolean isEmpty();
        void clear();
        boolean contains(T item);
        boolean add(T item);
        boolean remove(T item);
        Iterator<T> iterator();
    }

實現Iterable介面的集合必須提供一個iterator方法,該方法返回一個Iterator型別的物件。在這個物件中會記錄當前元素的位置。當我們使用迭代器遍歷集合元素時就需要使用到該方法。

public interface Iterator<E>{
        boolean hasNext();
        E next();
        void remove();
    }

iterator介面中有三個方法用於我們迭代元素。其中hasNext用來告訴是否存在下一項,next將會返回當前位置的元素,並隨後將當前位置後移,remove可用於移除元素。

這裡需要強調的是,我們應該避免使用Collection中的remove方法,而儘量使用iterator中的remove方法。因為當我們使用Collection中的remove方法時需要重新去索引元素的位置,而iterator自身儲存了當前元素的位置,因而可以減少索引時間上的開銷。

下面是迭代器的使用方法:

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

ArrayList 類的實現

這裡我將編寫ArrayList類的底層實現,為了避免與java.util中的類庫相混,這裡我將類命名成ArrayListTest

在我們編寫的ArrayListTest類中有以下幾處細節:

  1. ArrayListTest將以陣列為底層實現,並且將陣列的初始大小設定為10,以及該類擁有當前陣列中元素的項數size。
  2. 在該類中我們使用ensureCapacity方法來對陣列進行擴容,通過獲得一個新陣列,並將老陣列拷貝到新陣列中改變陣列的容量,允許虛擬機器回收老陣列。
  3. ArrayListTest類提供getset等實現。
  4. ArrayListTest類提供容器基本使用方法。如size()isEmpty()clear()方法,以及remove()add()對元素進行操作。
  5. 我們在這裡實現了Iterator介面的類,並重寫了其中的hasNextnextremove方法

ArrayListTest類的完整程式碼如下:

public class ArrayListTest<T> implements Iterable<T>{
    private static final int DEFAULT_CAPACITY = 10;
    private int size;
    private T[] items;
    public ArrayListTest(){
        doClear();
    }
    public void clear(){
        doClear();
    }
    public int size(){
        return size;
    }
    private void doClear(){
        size = 0;
        ensureCapacity(DEFAULT_CAPACITY);
    }
    public void ensureCapacity(int newSize){
        if(newSize<size){
            return;
        }
        T[] old = items;
        items = (T[]) new Object[newSize];
        for(int i=0;i<size();i++){
            items[i] = old[i];
        }
    }
    public boolean add(T x){
        add(size(), x);
        return true;
    }
    public boolean isEmpty(){
        return size() == 0;
    }
    public T get(int index){
        if(index < 0 || index >= size){
            throw new IndexOutOfBoundsException();
        }
        return items[index];
    }
    public T set(int index, T x){
        if(index < 0 || index >= size){
            throw new IndexOutOfBoundsException();
        }
        T old = items[index];
        items[index] = x;
        return old;
    }
    public void add(int id, T x){
        if(items.length == size){
            ensureCapacity(size() * 2 +1);
        }
        for(int i=size;i>id;i--){
            items[i] = items[i-1];
        }
        items[id] = x;
        size++;
    }
    public T remove(int index){
        T removeItem = items[index];
        int i=index;
        while (i<size()-1){
            items[i] = items[i+1];
            i++;
        }
        items[i] = null;
        size--;
        return removeItem;
    }
    @Override
    public String toString() {
        StringBuilder str = new StringBuilder("ArrayListTest{items=[");
        int i=0;
        while (i<size()){
            if(items[i] != null){
                str.append(items[i]).append(",");
            } else {
                break;
            }
            i++;
        }
        if(isEmpty()){
            return str + "]}";
        }
        return str.substring(0,str.length()-1) + "]}";
    }
    @Override
    public Iterator iterator() {
        return new ArrayListIterator();
    }
    private class ArrayListIterator implements Iterator{
        private int current = 0;
        @Override
        public boolean hasNext() {
            return current<size();
        }
        @Override
        public T next() {
            if(!hasNext()){
                throw new NoSuchElementException();
            }
            return items[current++];
        }
         @Override
        public void remove() {
            ArrayListTest.this.remove(--current);
        }
    }
}

在其中33行處可以看到這樣一行語句items = (T[]) new Object[newSize],因為泛型陣列的建立是非法的,我們的做法是建立了一個Object型別的陣列,然後通過泛型進行強制轉換。這將產生一個編譯器警告,但是泛型集合的實現中這是不可避免的。

在其中的add方法中,如果新增元素達到了當前陣列的容量大小後就需要對陣列進行擴容,由於擴充容量的代價是非常昂貴的,於是在這裡我將對擴容後的大小設定為原來大小的兩倍,以避免頻繁擴容帶來的代價。

下面是對於ArrayListTest類的測試:

public class Main {
    public static void main(String[] args) {
       ArrayListTest<Integer> arrayList = new ArrayListTest<Integer>();
       for(int i=0;i<10;i++){
           arrayList.add(i);
       }
       System.out.println("ArrayListTest:" + arrayList);
       arrayList.remove(1);
       System.out.println("remove:" + arrayList);
       arrayList.set(1,100);
       System.out.println("set:" + arrayList);
       System.out.println("get:" + arrayList.get(1));
       arrayList.clear();
       System.out.println("clear:" + arrayList);
    }

輸出結果如下:

ArrayListTest:ArrayListTest{items=[0,1,2,3,4,5,6,7,8,9]}
remove:ArrayListTest{items=[0,2,3,4,5,6,7,8,9]}
set:ArrayListTest{items=[0,100,3,4,5,6,7,8,9]}
get:100
clear:ArrayListTest{items=[]}

LinkedList 類的實現

LinkedList的實現相對於ArrayList的實現要過於複雜一點。由於它是將雙鏈表作為實現,而且還需要儲存連結串列兩端的引用。同ArrayList一樣,為了避免與類庫中的類相混,這裡我將類命名成LinkedListTest

LinkedListTest類有以下細節需要注意:

  1. 由於底層實現是連結串列形式,因此我們需要建立一個Node類作為連結串列中的每個結點。該節點包含資料以及到前一個結點的鏈(next鏈)和到後一個結點的鏈(previous鏈)。
  2. 在該實現中我們需要建立兩個空節點作為連結串列的兩端。
  3. 這裡仍然包含大多數操作方法,如addsetget等等。
  4. 在這裡實現的iterator介面中,我們會對每次addremove等操作過程進行監控,如果預期的modCount和實際的modCount不一樣,則在迭代中將丟擲異常。

LinkedListTest類的完整程式碼如下:

public class LinkedListTest<T> implements Iterable {
    private int size;
    private int modCount = 0;
    private Node<T> beginNode;
    private Node<T> endNode;
    //結點類
    private class Node<T> {
        public T data;
        public Node<T> previous;
        public Node<T> next;
        public Node(T data, Node<T> p, Node<T> n) {
            this.data = data;
            this.previous = p;
            this.next = n;
        }
    }
    public LinkedListTest() {
        doClear();
    }
    public void doClear() {
        clear();
    }
    public void clear() {
        beginNode = new Node<T>(null, null, null);
        endNode = new Node<T>(null, beginNode, null);
        beginNode.next = endNode;
        size = 0;
        modCount++;
    }
    public int size() {
        return size;
    }
    public boolean isEmpty() {
        return size() == 0;
    }
    public boolean add(T x) {
        add(size(), x);
        return true;
    }
    public void add(int index, T x) {
        addBefore(getNode(index, 0, size()), x);
    }
    public T get(int index) {
        return getNode(index).data;
    }
    public T set(int index, T newVal) {
        Node<T> node = getNode(index);
        T oldData = node.data;
        node.data = newVal;
        return oldData;
    }
    public T remove(int index) {
        return remove(getNode(index));
    }
    public T remove(Node<T> node) {
        node.next = node.previous.next;
        node.previous = node.next.previous;
        size--;
        modCount++;
        return node.data;
    }
    //在thisNode結點前插入一個結點
    private void addBefore(Node<T> thisNode, T x) {
        Node<T> newNode = new Node<T>(x, thisNode.previous, thisNode);
        newNode.previous.next = newNode;
        thisNode.previous = newNode;
        size++;
        modCount++;
    }
    private Node<T> getNode(int index) {
        return getNode(index, 0, size() - 1);
    }
    private Node<T> getNode(int index, int lower, int upper) {
        Node<T> thisNode;
        if (index < lower || index > upper) {
            throw new IndexOutOfBoundsException();
        }
        if (index < size() / 2) {
            thisNode = beginNode.next;
            for (int i = 0; i < index; i++) {
                thisNode = thisNode.next;
            }
        } else {
            thisNode = endNode;
            for (int i = size(); i > index; i--) {
                thisNode = thisNode.previous;
            }
        }
        return thisNode;
    }
    @Override
    public String toString() {
        StringBuilder str = new StringBuilder("LinkedListTest{item=");
        for (int i = 0; i < size; i++) {
            str.append(get(i)).append(",");
        }
        return str.substring(0, str.length() - 1) + "}";
    }
    @Override
    public Iterator iterator() {
        return new LinkedListIterator();
    }
    private class LinkedListIterator implements Iterator {
        private Node<T> current = beginNode.next;
        private int expectedModCount = modCount;
        private boolean okToRemove = false;
        @Override
        public boolean hasNext() {
            return current != endNode;
        }
        @Override
        public Object next() {
            if (modCount != expectedModCount) {
                throw new ConcurrentModificationException();
            }
            if (!hasNext()) {
                throw new NoSuchElementException();
            }
            T data = current.data;
            current = current.next;
            okToRemove = true;
            return data;
        }
        @Override
        public void remove() {
            if (modCount != expectedModCount) {
                throw new ConcurrentModificationException();
            }
            if (!okToRemove) {
                throw new IllegalStateException();
            }
            LinkedListTest.this.remove(current.previous);
            expectedModCount++;
            okToRemove = false;
        }
    }
}

在這個示例中,每次我們建立一個LinkedListTest時就會首先建立兩個空節點beginNodeendNode,並將beginNode的下一個結點與endNode相連。當每次進行新增元素時,只需要修改前後兩個結點的鏈之間的關係即可,如81-87行程式碼所示。

在其中有個getNode方法,該方法會對需要索引的元素位置進行判斷,如果該位置位於連結串列的前半部分,則從表的前端進行索引,否則從尾部進行索引。

在實現的Iterator中,LinkedListIterator使用到類似於ArrayListIterator的邏輯。在該迭代器中仍保留著一個當前位置current,它表示當前連結串列中的第一個元素。注意,當current被定位到endNode時,對next的呼叫時非法的。

為了防止在迭代過程期間集合元素被修改,我們在迭代器中將對modCount進行判斷,每次進行addremove等方法時都會對modCount進行加一,因此我們只需要在迭代過程中判斷modCount和expectedModCount的值是否相同,就可保證迭代過程中集合元素的完整性。

下面是對於LinkedListTest類的簡單測試:

public class Main {
    public static void main(String[] args) {
        LinkedListTest<String> linkedList = new LinkedListTest<String>();
        linkedList.add("111");
        linkedList.add("222");
        linkedList.add("333");
        System.out.println(linkedList);
    }
}

嘿嘿,輸出結果如下:

LinkedListTest{item=111,222,333}

總結

對於平常使用集合的過程中,我們並不會發現許多的細節,也只有當我們深入瞭解了集合的底層實現後才能更好、更正確的使用集合類。以上就是我對於表的兩種基本實現的總結。