動態數組、棧、隊列

動態數組

API介紹

數組是一種根據下標操作的數據結構,它的查詢速度很快,但是它有缺點,那就是數組的容量一旦在創建時確定,就不能進行更改,所以爲了克服這一缺點,我們實現一個自己的數組,併除此以外,還會實現一些方法,包括以下

  • add(int index, E e)
    • 向指定index添加元素e
  • get(int index)
    • 獲得指定index的元素
  • remove(int index)
    • 刪除指定index的元素並返回該元素
  • set(int index, E e)
    • 更改index處的元素爲e
  • getSize()
    • 返回數組中元素的個數
  • contains(E e)
    • 查詢數組是否包含元素e
  • isEmpty()
    • 查看數組是否爲空(是否有元素)
  • find(E e)
    • 返回數組中元素e第一次出現的index,若沒有元素e,則返回-1

新建一個Array類,它含有兩個私有成員變量

  • E[] data
    • 用以保存數據
  • int size
    • 用以記錄數組中元素的個數

除此以外還有兩個構造方法

  • Array(int capacity)
    • 設定數組的容量
  • Array()
    • 容量默認爲10
public class Array<E> {
    private E[] data;
    private int size;

    public Array(int capacity) {
        data = (E[]) new Object[capacity];
        size = 0;
    }

    public Array() {
        this(10);
    }
}

現在我們來實現上面提到的方法。

方法實現

首先來實現getSize()方法,這個是返回數組元素的個數的,我們直接返回size即可

public int getSize() {
    return size;
}

isEmpty()是爲了查看數組中是否還有元素,如果size0的話說明數組爲空,所以我們返回size == 0即可

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

現在來實現add(int index, E e)方法,該方法的實現是將index後面的元素都向後移動一位,然後在index處插入元素e

public void add(int index, E e) {
    //對inex進行驗證 如果不符合規範則拋出異常
    if (index < 0 || index > size) {
        throw new IllegalArgumentException("參數錯誤");
    }
    //將元素向後移動
    for (int i = size; i > index; i--) {
        data[i] = data[i - 1];
    }
    
    //在index處插入元素e
    data[index] = e;
    //數組中元素個數+1
    size++;
}

根據這個方法,我們可以很快的實現addFirst(E e)addLast(E e)方法,這兩個方法一個是在數組頭添加元素,一個是在數組的末尾添加一個元素

public void addLast(E e) {
    //在index = size處添加元素 即在數組末尾添加一個元素
    add(size,e);
}
public void addFirst(E e) {
    //在index = 0處添加一個元素 即在數組頭添加一個元素
    add(0,e);
}

下面來實現remove(int index)方法,該方法是刪除index處的元素,並將該元素返回,以添加的操作相反,刪除是將後面的元素向前移動,覆蓋掉index處的元素即可刪除

public E remove(int index) {
    //參數檢查
    if (index < 0 || index >= size) {
        throw new IllegalArgumentException("參數錯誤");
    }
    //獲得index處的元素用以返回
    E e = data[index];
    //將元素從後向前移一個
    for (int i = index; i < size - 1; i++) {
        data[i] = data[i+1];
    }
    //數組中元素個數-1
    size --;
    
    //返回刪除的元素
    return e;
}

同理,根據這個方法我們可以快速的實現removeLast()removeFirst()方法

public E removeLast() {
    return remove(size -1);
}
public E removeFirst() {
    return remove(0);
}

我們可以添加一個刪除指定元素的方法removeElement(E e),我們會遍歷數組,如果發現有元素等於該元素,那麼刪除該元素並退出方法,所以這個方法只刪除第一個元素e,並不是數組所有的元素e

public void removeElement(E e) {
    //遍歷數組
    for (int i = 0; i < size; i++) {
        //如果找到等於該元素的元素
        if (e.equals(data[i])) {
            //刪除該元素
            remove(i);
            //退出方法
            return;
        }
    }
}

下面實現contains(E e)方法,這個方法的思路同刪除指定元素相似,遍歷數組,如果找到元素與指定元素相同,那麼返回true,如果遍歷完數組還沒有找到與之相等的元素,那麼返回false

public boolean contains(E e) {
    //遍歷數組
    for (int i = 0; i < size; i++) {
        //如果找到元素,那麼返回true
        if (data[i].equals(e)) {
            return true;
        }
    }
    //如果遍歷完所有數組沒有找到,那麼返回false
    return false;
}

find(E e)方法的實現也是遍歷數組,如果找到了元素,那麼返回下標,如果遍歷完數組都沒有找到,那麼返回-1

public int find(E e) {
    //遍歷數組
    for (int i = 0; i < size; i++) {
        //找到元素則返回下標
        if (data[i].equals(e)) {
            return i;
        }
    }
    //如果遍歷完數組都沒有找到,返回-1
    return -1;
}

下面實現get(int index)set(int index, E e),這兩個方法的實現及其簡單,直接上代碼

public E get(int index) {
    if (index < 0 || index >= size) {
        throw new IllegalArgumentException("參數錯誤");
    }
    
    return data[index];
}

public void set(int index, E e) {
    if (index < 0 || index >= size) {
        throw new IllegalArgumentException("參數錯誤");
    }
    
    data[index] = e;
}

我們可以根據get方法實現getLast()getFirst()方法

public E getFirst() {
    return get(0);
}
public E getLast() {
    return get(size - 1);
}

現在我們已經實現了API中提到的所有的方法,但是我們還是沒有解決數組容量固定的問題,爲了解決這個問題,我們需要實現一個resize(int newCapacity),它的作用是該表數組的容量大小,這樣當數組的容量不足時,我們調用該方法就可以將數組進行擴容,或者當數組中有大量空間空閒時,我們可以縮小數組的容量,代碼如下

private void resize(int newCapacity) {
    //創建一個新容量的數組
    E[] temp = (E[]) new Object[newCapacity];
    //將數組中的數據全部放入新數組中
    for (int i =0; i < size; i++) {
        temp[i] = data[i];
    }
    //改變數組指針指向
    data = temp;
}

現在我們改變add(int index, E e)remove(int index)方法,我們會在添加元素和刪除元素時檢查數組的容量,以便對數組進行擴容或者縮容

public void add(int index, E e) {
    if (index < 0 || index > size) {
        throw new IllegalArgumentException("參數錯誤");
    }
    //如果數組容量滿了 那麼將數組的容量擴爲原來的兩倍
    if (size == data.length) {
        resize(data.length * 2);
    }
    for (int i = size; i > index; i--) {
        data[i] = data[i - 1];
    }
    data[index] = e;
    size++;
}
public E remove(int index) {
    if (index < 0 || index >= size) {
        throw new IllegalArgumentException("參數錯誤");
    }
    E e = data[index];
    for (int i = index; i < size - 1; i++) {
        data[i] = data[i+1];
    }
    size --;
    //如果數組中的元素個數爲數組容量的1/4,那麼容量變爲原來的1/2
    //思考一下爲什麼是1/4 提示:複雜度震盪
    if (size == data.length/4) {
        resize(data.length/2);
    }
    return e;
}

爲了方便的打印Array類,我們重寫toString()方法如下

public String toString() {
    StringBuilder str = new StringBuilder();
    str.append("size " + size);
    str.append(" capacity " + data.length);
    str.append("\n[");
    for (int i = 0; i < size; i++) {
        if (i == size - 1) {
            str.append(data[i].toString());
        } else {
            str.append(data[i].toString() + ", ");
        }
    }
    str.append("]");
    return str.toString();
}

至此,我們已經完全實現了Array,它的容量沒有限制,並且提供了很多的方法供用戶調用,我們將使用該類來實現其它的基本的數據結構。下面貼出完整的代碼

public class Array<E> {
    private E[] data;
    private int size;

    public Array(int capacity) {
        data = (E[]) new Object[capacity];
        size = 0;
    }

    public Array() {
        this(10);
    }

    public int getSize() {
        return size;
    }

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

    public void addLast(E e) {
        add(size,e);
    }
    public void addFirst(E e) {
        add(0,e);
    }

    public void add(int index, E e) {

        if (index < 0 || index > size) {
            throw new IllegalArgumentException("參數錯誤");
        }

        if (size == data.length) {
            resize(data.length * 2);
        }

        for (int i = size; i > index; i--) {
            data[i] = data[i - 1];
        }

        data[index] = e;
        size++;
    }

    public E removeLast() {
        return remove(size -1);
    }
    public E removeFirst() {
        return remove(0);
    }

    public E remove(int index) {
        if (index < 0 || index >= size) {
            throw new IllegalArgumentException("參數錯誤");
        }

        E e = data[index];
        for (int i = index; i < size - 1; i++) {
            data[i] = data[i+1];
        }
        size --;

        if (size == data.length/4) {
            resize(data.length/2);
        }

        return e;
    }

    public void removeElement(E e) {
        for (int i = 0; i < size; i++) {
            if (e.equals(data[i])) {
                remove(i);
                return;
            }
        }
    }

    public boolean contains(E e) {
        for (int i = 0; i < size; i++) {
            if (data[i].equals(e)) {
                return true;
            }
        }

        return false;
    }

    public int find(E e) {
        for (int i = 0; i < size; i++) {
            if (data[i].equals(e)) {
                return i;
            }
        }
        return -1;
    }

    private void resize(int newCapacity) {
        E[] temp = (E[]) new Object[newCapacity];
        for (int i =0; i < size; i++) {
            temp[i] = data[i];
        }
        data = temp;
    }

    public E get(int index) {
        if (index < 0 || index >= size) {
            throw new IllegalArgumentException("參數錯誤");
        }

        return data[index];
    }

    public E getFirst() {
        return get(0);
    }

    public E getLast() {
        return get(size - 1);
    }

    public void set(int index, E e) {
        if (index < 0 || index >= size) {
            throw new IllegalArgumentException("參數錯誤");
        }

        data[index] = e;
    }

    public String toString() {
        StringBuilder str = new StringBuilder();

        str.append("size " + size);
        str.append(" capacity " + data.length);
        str.append("\n[");

        for (int i = 0; i < size; i++) {
            if (i == size - 1) {
                str.append(data[i].toString());
            } else {
                str.append(data[i].toString() + ", ");
            }
        }

        str.append("]");

        return str.toString();
    }
}

棧是一種先進後出的結構,比如你放書會把書放在最上面,最先放的書在最下面,而你拿書卻是從最上面拿,最後放的最先拿到,棧正是怎麼一種結構,我們規定最上面的位置叫做棧頂,我們向棧中添加元素是添加到棧頂,向棧中取出元素是從棧頂取出的,我們先來定義一個Stack接口,裏面規定了一個棧包含的操作

public interface Stack<E> {
    //向棧中壓入一個元素
    void push(E e);
    //將棧頂元素彈出
    E pop();
    //棧是否爲空
    boolean isEmpty();
    //獲得棧中元素的個數
    int getSize();
    //獲得棧頂元素
    E peek();
}

下面我們將使用上面實現的Array來實現一個ArrayStack,我們把數組的最後位置定義爲棧頂

public class ArrayStack<E> implements Stack<E> {
    private Array<E> data;

    public ArrayStack(int capacity) {
        data = new Array<>(capacity);
    }

    public ArrayStack() {
        data = new Array<>();
    }

    @Override
    public void push(E e) {
        data.addLast(e);
    }

    @Override
    public E pop() {
        return data.removeLast();
    }

    @Override
    public boolean isEmpty() {
        return data.isEmpty();
    }

    @Override
    public int getSize() {
        return data.getSize();
    }

    @Override
    public E peek() {
        return data.getLast();
    }

    public String toString() {
        StringBuilder res = new StringBuilder();
        res.append("Stack: ");
        res.append("[");

        for (int i = 0; i < data.getSize(); i++) {
            res.append(data.get(i));
            if (i != data.getSize()-1) {
                res.append(", ");
            }
        }
        res.append("] top");

        return res.toString();
    }
}

上面的代碼極其的簡單,只要仔細的閱讀就可以完全的理解,這裏不多做解釋。

下面介紹一個有關於棧的題目,此題來自於LeetCode20

給定一個只包括’(’,’)’,’{’,’}’,’[’,’]’ 的字符串,判斷字符串是否有效。有效字符串需滿足:

1. 左括號必須用相同類型的右括號閉合。
2. 左括號必須以正確的順序閉合。

注意空字符串可被認爲是有效字符串。

這道題的解題思路是,如果遇到左括號’(’, ‘[’, ‘{’,那麼將左括號壓入棧中,如果遇到右括號,那麼將棧頂的左括號彈出,判斷兩個括號是否匹配,如果不匹配返回fasle,如果匹配進行下一輪,最後如果字符串遍歷完畢,如果棧爲空說明匹配成功,如果棧不爲空,所以左邊的括號多匹配失敗,代碼如下

import java.util.Stack;

class Solution {
    public boolean isValid(String s) {
        //創建一個空棧
        Stack<Character> stack = new Stack<>();
		
        //遍歷字符串
        for (int i = 0; i < s.length(); i++) {
            char c = s.charAt(i);
            //如果是左括號,則壓入棧中
            if (c == '(' || c == '[' || c == '{') {
                stack.push(c);
            } else {
                //如果是右括號 先判斷棧是否爲空
                if (stack.isEmpty()) {
                    return false;
                }
				
                //獲得棧頂的左括號
                char charTop = stack.pop();
                //下面三種皆爲不匹配的情況
                if (c == ')' && charTop != '(') {
                    return false;
                }
                if (c == ']' && charTop != '[') {
                    return false;
                }
                if (c == '}' && charTop != '{') {
                    return false;
                }
            }
        }
		
        //這裏不能直接返回true 要根據棧是否爲空決定返回值
        return stack.isEmpty();
    }
}

隊列

隊列是一種先進先出的結構,假設你在排隊,那麼最先排隊的人最先得到服務。我們只能從隊尾添加元素,從隊首取出元素。老規矩,我們首先規定一下隊列QueueAPI

public interface Queue<E> {
    //向隊列中添加一個元素
    void enqueue(E e);
    //從隊列中取出一個元素
    E dequeue();
    //獲得隊首的元素
    E getFront();
    //獲取隊列中元素的個數
    int getSize();
    //判斷隊列是否爲空
    boolean isEmpty();
}

數組隊列

現在我們將使用動態數組Array類來實現隊列,實現的邏輯也十分的簡單,如下

public class ArrayQueue<E> implements Queue<E> {
    private Array<E> array;

    public ArrayQueue() {
        array = new Array<>();
    }

    public ArrayQueue(int capacity) {
        array = new Array<>(capacity);
    }

    @Override
    public void enqueue(E e) {
        array.addLast(e);
    }

    @Override
    public E dequeue() {
        return array.removeFirst();
    }

    @Override
    public E getFront() {
        return array.getFirst();
    }

    @Override
    public int getSize() {
        return array.getSize();
    }

    @Override
    public boolean isEmpty() {
        return array.isEmpty();
    }

    @Override
    public String toString() {
        StringBuilder res = new StringBuilder();
        res.append("Queue: ");
        res.append("front [");

        for (int i = 0; i < array.getSize(); i++) {
            res.append(array.get(i));
            if (i != array.getSize()-1) {
                res.append(", ");
            }
        }
        res.append("] tail");

        return res.toString();
    }
}

注意上面我們的dequeue操作是調用了動態數組的removeFirst操作,這個操作需要遍歷整個數組將元素向前移動,所以該操作是O(n)的。

循環隊列

上面隊列的dequeue操作是O(n)級別的,這是因爲上面會將數組整體向前移一位,但是如果我們不這麼做,而是增加一個變量front來記錄隊首的位置,這樣我們只要將front向前移一位即可,這樣的操作就是O(1)級別的

這樣做的同時,我們發現,如果當tail來到數組的末尾,按道理應該將數組進行擴容,但是front前面還有空間

這個時候我們應當將tail移動到數組頭去

這時tail的計算公式不再是簡單的tail = tail + 1,而是tail = (tail + 1) % data.length,如果不理解這個式子,就想象一下時鐘,11點向前一步就是12點,也可以稱爲是0點,這個時候時鐘的計算公式爲(11 + 1) % 12。因爲這種循環的特性,我們把這種實現方式稱爲循環隊列。這次我們實現隊列不在使用上面的動態數組,有了上面實現棧和隊列的經驗,想必可以容易理解下面的代碼(在關鍵的步驟給予註釋)

public class LoopQueue<E> implements Queue<E> {
    private int front;
    private int tail;
    //隊列中元素的個數
    private int size;
    //底層實現的數組
    private E[] data;
	
    //構造方法初始化
    public LoopQueue(int capacity) {
        data = (E[]) new Object[capacity];
        size = 0;
        front = 0;
        tail = 0;
    }
    //默認容量爲10
    public LoopQueue() {
        this(10);
    }
	
    @Override
    public void enqueue(E e) {
        //首先判斷數組是不是滿了,如果是那麼就進行擴容
        if (size == data.length) {
            resize(2 * data.length);
        }
		
        //向隊尾添加元素
        data[tail] = e;
        //tail向後移動 不是簡單的+1 上面已有解釋
        tail = (tail +1) % data.length;
        size++;
    }
	
    //數組伸縮操作,已接觸過
    private void resize(int newCapacity) {
        E[] temp = (E[]) new Object[newCapacity];
        for (int i =0; i < size; i++) {
            //這裏我們將隊列的頭對應到新數組的開頭
            temp[i] = data[(front + i)%data.length];
        }
        //重新記錄front和tail的位置
        front = 0;
        tail = size;
        data = temp;
    }

    @Override
    public E dequeue() {
        //如果隊列爲空,拋出異常
        if (size == 0) {
            throw new IllegalArgumentException("隊列爲空");
        }
		
        //獲得出隊的元素
        E e = data[front];
        data[front] = null;
		
        //front向前移動(帶循環)
        front = (front + 1) % data.length;
        size--;
		
        //縮容操作,不做解釋
        if (size == data.length / 4) {
            resize(data.length / 2);
        }

        return e;
    }

    @Override
    public E getFront() {
        if (size == 0) {
            throw new IllegalArgumentException("隊列爲空");
        }

        return data[front];
    }

    @Override
    public int getSize() {
        return size;
    }

    @Override
    public boolean isEmpty() {
        return size == 0;
    }

    @Override
    public String toString() {
        StringBuilder str = new StringBuilder();
        str.append("Queue: size " + size);
        str.append(" capacity " + data.length);
        str.append("\nfront [");
        for (int i = 0; i < size; i++) {
            if (i == size - 1) {
                str.append(data[(front + i) % data.length].toString());
            } else {
                str.append(data[(front + i) % data.length].toString() + ", ");
            }
        }
        str.append("] tail");
        return str.toString();
    }
}

這次我們得到的dequeue操作就是O(1)的了(嚴格的講均攤複雜度爲O(1),因爲裏面resize()複雜度是O(n)的)。

發佈了113 篇原創文章 · 獲贊 44 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章