Java關於數據結構的實現:表、棧與隊列

關於做者java

郭孝星,程序員,吉他手,主要從事Android平臺基礎架構方面的工做,歡迎交流技術方面的問題,能夠去個人Github提issue或者發郵件至guoxiaoxingse@163.com與我交流。node

文章目錄git

  • 一 表的概念與應用場景
    • 1.1 數組
    • 1.2 鏈表
    • 1.3 棧
    • 1.4 隊列
  • 二 表的操做與源碼實現
    • 2.1 ArrayList實現原理
    • 2.2 LinkedList實現原理

更多文章:github.com/guoxiaoxing…程序員

一 數據結構與應用場景

咱們將形如A1,A2,A3,A4 ... AN稱之爲一個表,大小爲0的表咱們稱之爲空表。github

經常使用的表以下:數組

  • 數組
  • 單向鏈表/雙向鏈表
  • 隊列/雙端隊列

1.1 數組

數組是最爲簡單的一種表,它在查找操做是線性時間的,可是插入與刪除則潛藏額外的開銷。安全

  • 若是插入的位置在第一個,則須要將後面的元素所有向後移動一位,時間複雜副爲O(N)
  • 若是插入的位置在最後一個,則無需移動其餘元素,時間複雜副爲O(1)

總結起來,插入/刪除的時間時間複雜度爲O(N)。bash

1.2 鏈表

鏈表由一系列節點組成,這些節點沒必要在內存中相連,每一個節點均含有表元素和到包含該元素後繼元的節點的鏈,稱爲next鏈。這樣的鏈表
稱爲單鏈表,另外,若是節點還包含指向它在鏈表中的前驅節點的鏈,則成該鏈表爲雙鏈表。數據結構

鏈表是爲了不插入/刪除帶來的額外開銷,咱們又引入了鏈表,其中,鏈表又能夠分爲單鏈表與雙鏈表。架構

1.3 棧

棧是限制插入和刪除只能在表的末端進行的表,也就是出棧與入棧操做。

棧是爲了在指定的位置就行插入和刪除。

應用場景

平衡符號

在咱們編寫代碼的時候,常常會遇寫錯了一個符號而報錯,例如[()]是正確的,而[(])就是錯誤的。複製代碼
  1. 初始化一個空棧,讀入字符到文件末尾。
  2. 若是字符是一個開放符號(例如:[),則入棧。
  3. 若是字符是一個封閉符號(例如:]),若是棧空則報錯,不然將棧元素彈出,若是彈出的元素不是對應的開放符號,則報錯。
  4. 在文件末尾,若是棧非空則報錯。

方法調用

咱們知道在Java中有個叫方法調用棧的東西,它會爲每一個執行的方法建立一個棧幀,用來存儲局部變量表,操做數棧,動態連接和方法出口等信息。這種場景與上面提到
的平衡符號十分類似,由於方法的調用和放回和符號的開閉很是類似。

每次調用方法都會往棧裏插入一個棧幀,方法返回是再將其出棧,若是咱們將方法設計成遞歸調用,錯誤的結束條件或者過於龐大的數據量可能會引發棧溢出。

1.4 隊列

隊列也是一種表,使用隊列時在一端進行插入而在另外一端進行刪除。

如同棧同樣,對於隊列而言任何表的實現都是合法的。

二 數據結構與源碼實現

說完了關於表的基本概念,咱們來聊一聊Java中對錶的基本實現。Java中用接口List來定義表的基本功能,包括增、刪、改、查等基本操做。

List接口定義以下:

注:鏈表、棧、隊列都是表,所以對於實現了表方法的ArrayList/LinkedList都支持者三種數據結構。

public interface List<E> extends Collection<E> {
    // Query Operations
    int size();
    boolean isEmpty();
    boolean contains(Object o);
    Iterator<E> iterator();
    Object[] toArray();
    <T> T[] toArray(T[] a);

    // Modification Operations
    boolean add(E e);
    boolean remove(Object o);

    // Bulk Modification Operations
    boolean containsAll(Collection<?> c);
    boolean addAll(Collection<? extends E> c);
    boolean addAll(int index, Collection<? extends E> c);
    boolean removeAll(Collection<?> c);
    boolean retainAll(Collection<?> c);
    void clear();


    // Comparison and hashing
    boolean equals(Object o);
    int hashCode();


    // Positional Access Operations
    E get(int index);
    E set(int index, E element);
    void add(int index, E element);
    E remove(int index);


    // Search Operations
    int indexOf(Object o);
    int lastIndexOf(Object o);


    // List Iterators
    ListIterator<E> listIterator();
    ListIterator<E> listIterator(int index);

    // View
    List<E> subList(int fromIndex, int toIndex);

    @Override
    default Spliterator<E> spliterator() {
        return Spliterators.spliterator(this, Spliterator.ORDERED);
    }

    default void replaceAll(UnaryOperator<E> operator) {
        Objects.requireNonNull(operator);
        final ListIterator<E> li = this.listIterator();
        while (li.hasNext()) {
            li.set(operator.apply(li.next()));
        }
    }
    default void sort(Comparator<? super E> c) {
        Collections.sort(this, c);
    }
}複製代碼

關於List接口,咱們有兩個經常使用的實現類。

  • ArrayList:提供了一種List ADT的可增加的實現,優勢在於get、set花費常量時間,缺點在於插入、刪除代價昂貴。
  • LinkedList:提供了一種List ADT的雙鏈表的實現。優勢在於插入、刪除操做開銷較小,缺點在於不用於作索引,get操做代價昂貴。

2.1 ArrayList實現原理

ArrayList提供了一種List ADT的可增加的實現,優勢在於get、set花費常量時間,缺點在於插入、刪除代價昂貴。

ArrayList是以數組爲基礎實現的線性數據結構,具體說來,它有如下特色:

  • 快速查找,在物理內存上採用順序存儲結構,能夠根據索引進行快速查找。
  • 容量動態增加:當數組容量不夠用時,建立一個比原來更大的數組,將原來數組的元素複製到新數組中。
  • 能夠插入null
  • 沒有作同步,非線程安全

ArrayList類圖以下所示:

實現瞭如下接口:

  • List:List ADT相關方法。
  • RandomAccess:隨機訪問功能,在ArrayList中經過索引快速獲取元素,這就是隨機訪問功能。
  • Cloneable:返回ArrayList的淺拷貝。
  • Serialiable:實現了序列化功能。

ArrayList採用數組存取元素。

//保存ArrayList數據的數組,它採用transient關鍵字標記,說明序列化ArrayList忽略掉該字段
transient Object[] elementData;

//數據數量
private int size;複製代碼

咱們接下來看看ArrayList關於增、刪、改、查的實現。

add

時間複雜度:O(N)

實現原理:ArrayList能夠在指定位置增長元素,增長元素後,當前元素後面的元素都要向後移動一位。

public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
    public boolean add(E e) {
        ensureCapacityInternal(size + 1);  // Increments modCount!!
        elementData[size++] = e;
        return true;
    }

    public void add(int index, E element) {
        if (index > size || index < 0)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

        ensureCapacityInternal(size + 1);  // Increments modCount!!

        //將數組elementData從index位置開始的全部元素,拷貝到elementData從index + 1位置開始的位置
        //也就是index位置後面的元素所有向後移動一位
        System.arraycopy(elementData, index, elementData, index + 1,
                         size - index);
        elementData[index] = element;
        size++;
    }
}複製代碼

ArrayList會調用ensureCapacityInternal()方法來保證數組容量的自動增加,咱們先來看看它的實現。

public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
   private void ensureExplicitCapacity(int minCapacity) {
           modCount++;

           // overflow-conscious code
           if (minCapacity - elementData.length > 0)
               grow(minCapacity);
       }

       //ArrayList內部數組的最大容量
       private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;

       private void grow(int minCapacity) {
           // overflow-conscious code
           int oldCapacity = elementData.length;
           int newCapacity = oldCapacity + (oldCapacity >> 1);
           if (newCapacity - minCapacity < 0)
               newCapacity = minCapacity;
           if (newCapacity - MAX_ARRAY_SIZE > 0)
               newCapacity = hugeCapacity(minCapacity);
           // minCapacity is usually close to size, so this is a win:
           elementData = Arrays.copyOf(elementData, newCapacity);
       } 
}複製代碼

能夠看到ArrayList調用grow(int minCapacity)方法來增長容量,這裏有個最小容量minCapacity,它等於當前數組大小size+1。該方法的
計算流程以下:

  1. 舊的容量 = 過去數組的大小
  2. 新的容量 = 過去容量 + 過去容量>>1
  3. 上一步計算的新容量若是小於最小容量則使用最小容量做爲新的容量
  4. 若是上一步獲得的容量大於最大數組容量,則使用最大數組容量做爲新的容量
  5. 將原有數組的數據拷貝到新數組,並賦值給elementData。

這裏咱們還要提到一個數組拷貝的方法,它是一個native方法。

public static native void arraycopy(Object src, int srcPos, Object dest, int destPos, int length);複製代碼
  • Object src:源數組
  • int srcPos:原數組開始拷貝的位置
  • Object dest:目標數組
  • int destPos:目標數組開始拷貝的位置
  • int length:被拷貝元素的數量

remove

時間複雜度:O(N)

實現原理:刪除指定位置的元素,刪除元素後,把該元素後面的全部元素向前移動一位

public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable{

    public E remove(int index) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

        modCount++;
        E oldValue = (E) elementData[index];

        int numMoved = size - index - 1;
        if (numMoved > 0)
            //當前刪除index後面的元素所有向前移動一位
            System.arraycopy(elementData, index+1, elementData, index,
                             numMoved);
        elementData[--size] = null; // clear to let GC do its work

        return oldValue;
    }

    public boolean remove(Object o) {
        if (o == null) {
            for (int index = 0; index < size; index++)
                if (elementData[index] == null) {
                    fastRemove(index);
                    return true;
                }
        } else {
            for (int index = 0; index < size; index++)
                if (o.equals(elementData[index])) {
                    fastRemove(index);
                    return true;
                }
        }
        return false;
    }
}複製代碼

ArrayList提供了兩種移除元素的方法,按索引移除與按對象移除,按對象移除會先去遍歷該對象的索引,而後再按索引移除。

set

時間複雜度:O(1)

實現原理:替換內部數組相應位置上的元素

public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
    public E set(int index, E element) {
        if (index >= size)
            throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

        E oldValue = (E) elementData[index];
        elementData[index] = element;
        return oldValue;
    }
}複製代碼

get

時間複雜度:O(1)

實現原理:根據指定索引獲取當前元素,無需額外的計算。

public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable{

   public E get(int index) {
       if (index >= size)
           throw new IndexOutOfBoundsException(outOfBoundsMsg(index));

       return (E) elementData[index];
   }
}複製代碼

2.1 LinkedList實現原理

LinkedList提供了一種List ADT的雙鏈表的實現。優勢在於插入、刪除操做開銷較小,缺點在於不用於作索引,get操做代價昂貴。

LinkedList基於雙向鏈表實現的,它具備以特色:

  • 基於雙向鏈表實現,能夠做爲鏈表使用,也能夠做爲棧、隊列和雙端隊列使用。
  • 沒有作同步,非線程安全

ArrayList類圖以下所示:

實現瞭如下接口:

  • List:List ADT相關方法。
  • Deque:能夠將LinkedList當作雙端隊列來使用。
  • Cloneable:返回ArrayList的淺拷貝。
  • Serialiable:實現了序列化功能。

LinkedList裏用節點來描述裏面的數據,以下所示:

private static class Node<E> {
    E item;
    Node<E> next;
    Node<E> prev;

    Node(Node<E> prev, E element, Node<E> next) {
        this.item = element;
        this.next = next;
        this.prev = prev;
    }
}複製代碼

能夠看到,每一個節點包含了當前的元素以及它的前驅節點和後繼節點。

LinkedList包含了三個成員變量:

//節點個數
transient int size = 0;

//第一個節點
transient Node<E> first;

//最後一個節點
transient Node<E> last;複製代碼

咱們接下來看看ArrayList關於增、刪、改、查的實現。

add

時間複雜度:add(E e) - O(1),add(int index, E element) - O(N)

實現原理:增長和刪除的過程都是一個針對指定節點進行前驅和後繼關係修改的過程,若是是在起始節點和末尾節點插入、整個過程無需額外的遍歷計算。
若是實在中間位置插入,則須要查找當前索引的節點。

原理圖以下:

public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable{

    public boolean add(E e) {
            linkLast(e);
            return true;
        }

    public void add(int index, E element) {
        checkPositionIndex(index);

        if (index == size)
            linkLast(element);
        else
            linkBefore(element, node(index));
    }

    //查找指定索引的節點
    Node<E> node(int index) {
        //根據index的大小決定是從起始節點,仍是從末尾節點開始查找
        if (index < (size >> 1)) {
            Node<E> x = first;
            for (int i = 0; i < index; i++)
                x = x.next;
            return x;
        } else {
            Node<E> x = last;
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            return x;
        }
    }

    private void linkFirst(E e) {
        final Node<E> f = first;
        //建立一個以起始節點last爲後繼的節點
        final Node<E> newNode = new Node<>(null, e, f);
        //將新建立的節點從新設置爲起始節點
        first = newNode;
        //若是起始節點爲空,則將新節點設置爲末尾,若是不爲空,則將起始節點的前驅指向新建立的節點
        if (f == null)
            last = newNode;
        else
            f.prev = newNode;
        //大小加自增1,修改字數自增1
        size++;
        modCount++;
    }

    //末尾插入元素
    void linkLast(E e) {
        final Node<E> l = last;
        //建立一個以末尾節點last爲前驅的節點
        final Node<E> newNode = new Node<>(l, e, null);
        //將新建立的節點從新設置爲末尾節點
        last = newNode;
        //若是末尾節點爲空,則將新節點設置爲起始節點,若是不爲空,則將末尾節點的後繼指向新建立的節點
        if (l == null)
            first = newNode;
        else
            l.next = newNode;
        //大小加自增1,修改字數自增1
        size++;
        modCount++;
    }

    void linkBefore(E e, Node<E> succ) {
        final Node<E> pred = succ.prev;
        //以原來節點的前驅做爲本身的前驅,以原來節點做爲本身的後繼,建立新的節點
        final Node<E> newNode = new Node<>(pred, e, succ);
        //將新的節點做爲原來節點的前驅
        succ.prev = newNode;
        //若是原來節點的前驅爲空,即它原來是起始節點,則直接將新建立的節點做爲新的起始節點,若是不爲空,則將
        //原來節點的前驅的後繼設置爲當前建立的新節點
        if (pred == null)
            first = newNode;
        else
            pred.next = newNode;
        //大小加自增1,修改字數自增1
        size++;
        modCount++;
    }
}複製代碼

咱們能夠在指定位置插入元素,若是沒有指定位置,默認在鏈表末尾插入元素。能夠看到add方法的實現是依賴於link方法來創建前驅與後繼的聯繫。

具體說來:

linkFirst

  1. 建立一個以起始節點last爲後繼的節點
  2. 將新建立的節點從新設置爲起始節點
  3. 若是起始節點爲空,則將新節點設置爲末尾,若是不爲空,則將起始節點的前驅指向新建立的節點
  4. 大小加自增1,修改字數自增1

linkBefore

  1. 以原來節點的前驅做爲本身的前驅,以原來節點做爲本身的後繼,建立新的節點
  2. 將新的節點做爲原來節點的前驅
  3. 若是原來節點的前驅爲空,即它原來是起始節點,則直接將新建立的節點做爲新的起始節點,若是不爲空,則將原來節點的前驅的後繼設置爲當前建立的新節點
  4. 大小加自增1,修改字數自增1

remove

時間複雜度:O(1)

實現原理:增長和刪除的過程都是一個針對指定節點進行前驅和後繼關係修改的過程,整個過程無需額外的遍歷計算。

原理圖以下:

public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable{

   public E remove() {
        return removeFirst();
    }

    public E remove(int index) {
        checkElementIndex(index);
        return unlink(node(index));
    }

    public boolean remove(Object o) {
        if (o == null) {
            for (Node<E> x = first; x != null; x = x.next) {
                if (x.item == null) {
                    unlink(x);
                    return true;
                }
            }
        } else {
            for (Node<E> x = first; x != null; x = x.next) {
                if (o.equals(x.item)) {
                    unlink(x);
                    return true;
                }
            }
        }
        return false;
    }

   public E removeLast() {
        final Node<E> l = last;
        if (l == null)
            throw new NoSuchElementException();
        return unlinkLast(l);
    }

    private E unlinkFirst(Node<E> f) {
        // assert f == first && f != null;
        final E element = f.item;
        final Node<E> next = f.next;
        f.item = null;
        f.next = null; // help GC
        first = next;
        if (next == null)
            last = null;
        else
            next.prev = null;
        size--;
        modCount++;
        return element;
    }

    private E unlinkLast(Node<E> l) {
        // assert l == last && l != null;
        final E element = l.item;
        final Node<E> prev = l.prev;
        l.item = null;
        l.prev = null; // help GC
        last = prev;
        if (prev == null)
            first = null;
        else
            prev.next = null;
        size--;
        modCount++;
        return element;
    }

    E unlink(Node<E> x) {
        //找出要刪除節點的前驅與後繼
        final E element = x.item;
        final Node<E> next = x.next;
        final Node<E> prev = x.prev;

        //若是前驅爲空,則將要刪除節點的後繼設置爲起始節點,不然將刪除節點的前驅的後繼指向刪除節點的後繼
        if (prev == null) {
            first = next;
        } else {
            prev.next = next;
            x.prev = null;
        }

        //若是後繼爲空,則直接將刪除節點的前驅做爲末尾節點,不然將刪除節點的後繼的前驅指向刪除節點的前驅
        if (next == null) {
            last = prev;
        } else {
            next.prev = prev;
            x.next = null;
        }
        //刪除元素置空,大小加自增1,修改字數自增1
        x.item = null;
        size--;
        modCount++;
        return element;
    }
}複製代碼

能夠看出,刪除的過程就是一個接觸前驅和後繼的過程。主要的有unlink方法來實現,具體說來:

unlink

  1. 找出要刪除節點的前驅與後繼
  2. 若是前驅爲空,則將要刪除節點的後繼設置爲起始節點,不然將刪除節點的前驅的後繼指向刪除節點的後繼
  3. 若是後繼爲空,則直接將刪除節點的前驅做爲末尾節點,不然將刪除節點的後繼的前驅指向刪除節點的前驅
  4. 刪除元素置空,大小加自增1,修改字數自增1

這麼描述有點繞,就是刪除原來的前驅後繼關係,從新創建鏈接。

set

時間複雜度:O(N)

實現原理:查找指定index對應節點,替換掉節點裏的元素,set操做也有個查找過程。

public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable{

    public E set(int index, E element) {
        checkElementIndex(index);
        Node<E> x = node(index);
        E oldVal = x.item;
        x.item = element;
        return oldVal;
    }

    //查找指定索引的節點
    Node<E> node(int index) {
        //根據index的大小決定是從起始節點,仍是從末尾節點開始查找
        if (index < (size >> 1)) {
            Node<E> x = first;
            for (int i = 0; i < index; i++)
                x = x.next;
            return x;
        } else {
            Node<E> x = last;
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            return x;
        }
    }
}複製代碼

get

時間複雜度:O(N)

實現原理:根據index的位置決定是從起始節點開始查找,仍是從末尾節點開始查找。

public class LinkedList<E> extends AbstractSequentialList<E> implements List<E>, Deque<E>, Cloneable, java.io.Serializable{

    public E get(int index) {
        checkElementIndex(index);
        return node(index).item;
    }

    //查找指定索引的節點
    Node<E> node(int index) {
        //根據index的大小決定是從起始節點,仍是從末尾節點開始查找
        if (index < (size >> 1)) {
            Node<E> x = first;
            for (int i = 0; i < index; i++)
                x = x.next;
            return x;
        } else {
            Node<E> x = last;
            for (int i = size - 1; i > index; i--)
                x = x.prev;
            return x;
        }
    }
}複製代碼

關於表、棧與隊列的內容咱們就講到這裏,後續有新的內容還會在這篇文章更新。

相關文章
相關標籤/搜索