動態數組
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()是爲了查看數組中是否還有元素,如果size爲0的話說明數組爲空,所以我們返回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();
}
}
上面的代碼極其的簡單,只要仔細的閱讀就可以完全的理解,這裏不多做解釋。
下面介紹一個有關於棧的題目,此題來自於LeetCode第20題
給定一個只包括’(’,’)’,’{’,’}’,’[’,’]’ 的字符串,判斷字符串是否有效。有效字符串需滿足:
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();
}
}
隊列
隊列是一種先進先出的結構,假設你在排隊,那麼最先排隊的人最先得到服務。我們只能從隊尾添加元素,從隊首取出元素。老規矩,我們首先規定一下隊列Queue的API
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)的)。