一般用O(1),O(n),O(lgn),O(nlogn),O(n^2)等表示算法時間複雜度,大O描述的是算法運行時間和輸入數據之間的關係。java
看一個對輸入數據進行求和的算法:算法
1 public static int sum(int[] nums) { 2 int sum = 0; 3 for(int num: nums) sum += num; 4 return sum; 5 }
第3行,對於nums中的每一個數,都要進行這種操做,執行時間咱們計爲常量c1;
第2行和第4行的執行時間計作常量c2;shell
得出該算法的運行時間與輸入數據(數組個數規模)之間是一種線性關係:數組
T = c1*n + c2
分析時間複雜度時,忽略常數。所以該算法的時間複雜度爲O(n)。數據結構
再看下面的關係:app
T1 = 2*n + 2 O(n) T2 = 2000*n + 10000 O(n) T3 = 1*n*n + 0 O(n^2)
咱們知道高階時間複雜度O(n^2)是大於低階時間複雜度O(n)的,可是當n等於10時,高階算法的執行時間T3=100,低階算法的執行時間T2=12000,T3 < T2,這不是矛盾了嗎?dom
其實,大O的表示的是漸進時間複雜度,描述的是n趨近於無窮時的狀況。在n趨於無窮時,T3 > T2。ide
對於n較小的狀況下,當高階算法的常數比較小的時候,有可能運行時間反而快於低階算法的
當n趨於無窮的狀況下,同時存在高階和低階時,低階是能夠被忽略的:函數
T1 = 300n + 10 O(n) T2 = 1*n*n + 300n + 10 O(n^2)
數組就是把數據碼成一排進行存放,是一種線性數據結構:
數組的最大優勢:快速查詢。scores[2]oop
咱們基於Java的靜態數組,封裝一個屬於本身的動態數組類Array,加深對於數組這種數據結構的理解。
咱們基於Java靜態數組data來封裝咱們的動態數組Array類,capacity表示數組容量,能夠經過data.length得到。size表示數組元素個數,初始爲0(也能夠當作是下一個新增元素的索引位置)。
據此,設計Array類結構。
初始數組類結構
public class Array<E> { private E[] data; private int size; // 構造函數,傳入數組的容量captacity構造Array public Array(int capacity) { data = (E[])new Object[capacity]; size = 0; } // 無參數的構造函數,默認數組的容量capacity=10 public Array() { this(10); } // 獲取數組中的元素個數 public int getSize() { return size; } // 獲取數組的容量 public int getCapacity() { return data.length; } // 返回數組是否爲空 public boolean isEmpty() { return size == 0; } }
向數組末尾添加元素
添加元素前:
添加元素後:
分析得出,若是要向數組添加元素e,只要在size所在索引位置設置元素e,而後size向後移動(size++)便可。
據此,增長添加元素相關的代碼:
// 在全部元素後添加一個新元素 public void addLast(E e) { if(size == data.length) { // 數組容量已填滿,則不能再添加 throw new IllegalArgumentException("AddLast failed. Array is full."); } data[size] = e; size++; }
向指定位置添加元素
添加前:
添加後:
分析得出,只要把要插入元素的索引位置的當前元素以及其後的全部元素,都日後挪動一位,而後在指定索引位置設置新元素便可,最後size++。爲避免元素覆蓋,具體挪的時候,須要從後往前推動完成元素挪動的整個過程。
修改代碼:
// 在第index個位置插入一個新元素e public void add(int index, E e) { if(size == data.length) { throw new IllegalArgumentException("Add failed. Array is full."); } if(index < 0 || index > size) { throw new IllegalArgumentException("Add failed. Require index >= 0 and index <= size."); } for(int i = size - 1; i >= index; i--) { data[i + 1] = data[i]; } data[index] = e; size++; }
調整addLast,複用add方法,同時增長一個addFirst:
// 在全部元素後添加一個新元素 public void addLast(E e) { add(size, e); } // 在全部元素前添加一個新元素 public void addFirst(E e) { add(0, e); }
獲取元素和修改元素
// 獲取index索引位置的元素 public E get(int index) { if (index < 0 || index >= size) { throw new IllegalArgumentException("Get failed. Index is illegal."); } return data[index]; } // 修改index索引位置的元素 public void set(int index, E e) { if (index < 0 || index >= size) { throw new IllegalArgumentException("Set failed. Index is illegal."); } data[index] = e; }
包含、搜索
// 查找數組中是否有元素e public boolean contains(E e) { for (int i = 0; i < size; i++) { if (data[i].equals(e)) { return true; } } return false; } // 查找數組中元素e所在的索引,若是不存在元素e,則返回-1 public int find(E e) { for (int i = 0; i < size; i++) { if (data[i].equals(e)) { return i; } } return -1; }
從數組中刪除元素
刪除前:
刪除後:
分析得出,只要將要刪除位置以後的元素都往前挪動一位便可。而後size減1。
修改代碼:
// 從數組中刪除index位置的元素,返回刪除的元素 public E remove(int index) { if (index < 0 || index >= size) { throw new IllegalArgumentException("Remove failed. Index is illegal."); } E ret = data[index]; for (int i = index + 1; i < size; i++) { data[i-1] = data[i]; } size--; return ret; } // 從數組中刪除第一個元素,返回刪除的元素 public E removeFirst() { return remove(0); } // 從數組中刪除最後一個元素,返回刪除的元素 public E removeLast() { return remove(size - 1); } // 從數組中刪除元素e(只刪除一個) public boolean removeElement(E e) { int index = find(e); if (index != -1) { remove(index); return true; } return false; }
調整爲動態數組
容量開太大,浪費空間,容量開小了,空間不夠用。因此須要實現動態數組。
具體作法以下:
就是在方法中開闢一個更大容量的數組,循環遍歷原來的數組元素,設置到新的數組上。而後將原數組的data指向新數組。
修改代碼:
// 數組容量擴容/縮容 public void resize(int newCapacity) { E[] newData = (E[])new Object[newCapacity]; for (int i = 0; i < size; i++) { newData[i] = data[i]; } data = newData; }
修改添加元素的代碼,添加時自動擴容:
// 在第index個位置插入一個新元素e public void add(int index, E e) { if (index < 0 || index > size) { throw new IllegalArgumentException("AddLast failed. Require index >= 0 and index <= size"); } if (size == data.length) { resize(2 * data.length); // 擴容爲原來的2倍 } for (int i = size - 1; i >= index; i--) { data[i + 1] = data[i]; } data[index] = e; size++; }
修改刪除元素的代碼,必要時自動縮容:
// 從數組中刪除index位置的元素,返回刪除的元素 public E remove(int index) { if (index < 0 || index >= size) { throw new IllegalArgumentException("Remove failed. Index is illegal."); } E ret = data[index]; for (int i = index + 1; i < size; i++) { data[i - 1] = data[i]; } size--; data[size] = null; // loitering objects != memory leak if (size == data.length / 2 && data.length / 2 != 0) { resize(data.length / 2); // 縮容爲原來的一半 } return ret; }
測試咱們的數組
@Override public String toString() { StringBuilder res = new StringBuilder(); res.append(String.format("Array: size = %d, capacity = %d\n", size, data.length)); res.append("["); for (int i = 0; i < size; i++) { res.append(data[i]); if (i != size - 1) { res.append(", "); } } res.append("]"); return res.toString(); } public static void main(String[] args) { Array<Integer> arr = new Array<>(); for (int i = 0; i < 10; i++) { arr.addLast(i); } System.out.println(arr); arr.add(1, 100); System.out.println(arr); arr.addFirst(-1); System.out.println(arr); arr.remove(2); System.out.println(arr); arr.removeElement(4); System.out.println(arr); arr.removeFirst(); System.out.println(arr); }
console輸出:
Array: size = 10, capacity = 10 [0, 1, 2, 3, 4, 5, 6, 7, 8, 9] Array: size = 11, capacity = 20 [0, 100, 1, 2, 3, 4, 5, 6, 7, 8, 9] Array: size = 12, capacity = 20 [-1, 0, 100, 1, 2, 3, 4, 5, 6, 7, 8, 9] Array: size = 11, capacity = 20 [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9] Array: size = 10, capacity = 20 [-1, 0, 1, 2, 3, 5, 6, 7, 8, 9] Array: size = 9, capacity = 20 [0, 1, 2, 3, 5, 6, 7, 8, 9]
綜合來看,
對於addLast(e)和removeLast(e),有可能會涉及到resize,因此仍是O(n)。可是,對於這種相對比較耗時的操做,若是能保證它不是每次都會觸發的話,能夠用均攤複雜度更爲合理。
均攤時間複雜度
假設capacity=n,n+1次addLast操做後,觸發resize操做,resize操做對n個元素進行復制,因此總共進行2n+1次操做。平均,每次addLast操做,進行2次基本操做。這種均攤計算,時間複雜度是O(1)。
同理removeLast(),均攤時間複雜度也是O(1)
複雜度震盪
上面,咱們按均攤時間複雜度來分析,addLast()和removeLast()操做的時間複雜度都是O(1)。
可是當咱們同時關注addLast()和removeLast()操做的時候,存在這麼一種狀況:假設capacity=n,當程序操做addLast()添加第n+1個元素的時候,觸發resize擴容,此時,時間複雜度爲O(n)。而後很不幸的,接着立刻又是removeLast()刪除第n+1個元素,又觸發了resize縮容,時間複雜度仍是O(n)。更不幸的是,這時候addLast()、removeLast()操做一直在反覆進行,那麼每次都是O(n)了。
對於這個問題,就是複雜度震盪,問題的關鍵在於removeLast()的時候縮容的有些着急(Eager)。能夠優化成延遲縮容,在數組元素只有容量的1/4的時候再進行縮容。
修改代碼以下:
// 從數組中刪除index位置的元素,返回刪除的元素 public E remove(int index) { if (index < 0 || index >= size) { throw new IllegalArgumentException("Remove failed. Index is illegal."); } E ret = data[index]; for (int i = index + 1; i < size; i++) { data[i - 1] = data[i]; } size--; data[size] = null; // loitering objects != memory leak // 縮容數組使用lazy方式(避免複雜度震盪),在1/4的時候才縮容 if (size == data.length / 4 && data.length / 2 != 0) { resize(data.length / 2); // 縮容爲原來的一半 } return ret; }
public class Array<E> { private E[] data; private int size; // 構造函數,傳入數組的容量captacity構造Array public Array(int capacity) { data = (E[])new Object[capacity]; size = 0; } // 無參數的構造函數,默認數組的容量capacity=10 public Array() { this(10); } // 獲取數組中的元素個數 public int getSize() { return size; } // 獲取數組的容量 public int getCapacity() { return data.length; } // 返回數組是否爲空 public boolean isEmpty() { return size == 0; } // 在第index個位置插入一個新元素e public void add(int index, E e) { if (index < 0 || index > size) { throw new IllegalArgumentException("Add failed. Require index >= 0 and index <= size"); } if (size == data.length) { resize(2 * data.length); // 擴容爲原來的2倍 } for (int i = size - 1; i >= index; i--) { data[i + 1] = data[i]; } data[index] = e; size++; } // 在全部元素後添加一個新元素 public void addLast(E e) { add(size, e); } // 在全部元素前添加一個新元素 public void addFirst(E e) { add(0, e); } // 獲取index索引位置的元素 public E get(int index) { if (index < 0 || index >= size) { throw new IllegalArgumentException("Get failed. Index is illegal."); } return data[index]; } // 修改index索引位置的元素 public void set(int index, E e) { if (index < 0 || index >= size) { throw new IllegalArgumentException("Set failed. Index is illegal."); } data[index] = e; } // 查找數組中是否有元素e public boolean contains(E e) { for (int i = 0; i < size; i++) { if (data[i].equals(e)) { return true; } } return false; } // 查找數組中元素e所在的索引,若是不存在元素e,則返回-1 public int find(E e) { for (int i = 0; i < size; i++) { if (data[i].equals(e)) { return i; } } return -1; } // 從數組中刪除index位置的元素,返回刪除的元素 public E remove(int index) { if (index < 0 || index >= size) { throw new IllegalArgumentException("Remove failed. Index is illegal."); } E ret = data[index]; for (int i = index + 1; i < size; i++) { data[i - 1] = data[i]; } size--; data[size] = null; // loitering objects != memory leak // 縮容數組使用lazy方式(避免複雜度震盪),在1/4的時候才縮容 if (size == data.length / 4 && data.length / 2 != 0) { resize(data.length / 2); // 縮容爲原來的一半 } return ret; } // 從數組中刪除第一個元素,返回刪除的元素 public E removeFirst() { return remove(0); } // 從數組中刪除最後一個元素,返回刪除的元素 public E removeLast() { return remove(size - 1); } // 從數組中刪除元素e(只刪除一個) public boolean removeElement(E e) { int index = find(e); if (index != -1) { remove(index); return true; } return false; } public void resize(int newCapacity) { E[] newData = (E[])new Object[newCapacity]; for (int i = 0; i < size; i++) { newData[i] = data[i]; } data = newData; } @Override public String toString() { StringBuilder res = new StringBuilder(); res.append(String.format("Array: size = %d, capacity = %d\n", size, data.length)); res.append("["); for (int i = 0; i < size; i++) { res.append(data[i]); if (i != size - 1) { res.append(", "); } } res.append("]"); return res.toString(); } public static void main(String[] args) { Array<Integer> arr = new Array<>(); for (int i = 0; i < 10; i++) { arr.addLast(i); } System.out.println(arr); arr.add(1, 100); System.out.println(arr); arr.addFirst(-1); System.out.println(arr); arr.remove(2); System.out.println(arr); arr.removeElement(4); System.out.println(arr); arr.removeFirst(); System.out.println(arr); } }
無處不在的撤銷操做、程序調用的調用棧,等等都是棧的常見應用。
基於咱們實現的動態數組Array來實現棧
public interface Stack<E> { int getSize(); boolean isEmpty(); void push(E e); E pop(); E peek(); }
public class ArrayStack<E> implements Stack<E> { Array<E> array; public ArrayStack(int capacity) { array = new Array<>(capacity); } public ArrayStack() { array = new Array<>(); } @Override public int getSize() { return array.getSize(); } @Override public boolean isEmpty() { return array.isEmpty(); } @Override public void push(E e) { array.addLast(e); } @Override public E pop() { return array.removeLast(); } @Override public E peek() { return array.getLast(); } public int getCapacity() { return array.getCapacity(); } @Override public String toString() { StringBuffer res = new StringBuffer(); res.append("Stack: "); res.append("["); for (int i = 0; i < array.getSize(); i++) { res.append(array.get(i)); if (i != array.getSize() -1) { res.append(", "); } } res.append("] top"); return res.toString(); } public static void main(String[] args) { ArrayStack<Integer> stack = new ArrayStack<>(); for (int i = 0; i < 5; i++) { stack.push(i); System.out.println(stack); } stack.pop(); System.out.println(stack); } }
輸出結果:
Stack: [0] top Stack: [0, 1] top Stack: [0, 1, 2] top Stack: [0, 1, 2, 3] top Stack: [0, 1, 2, 3, 4] top Stack: [0, 1, 2, 3] top
因爲基於咱們的動態數組Array來實現的棧,因此該棧也具有了縮容和擴容的能力。
ArrayStack<E>
基於咱們實現的動態數組Array來實現隊列
public interface Queue<E> { int getSize(); boolean isEmpty(); void enqueue(E e); E dequeue(); E getFront(); }
public class ArrayQueue<E> implements Queue<E> { private Array<E> array; public ArrayQueue(int capacity) { array = new Array<>(capacity); } public ArrayQueue() { array = new Array<>(); } @Override public int getSize() { return array.getSize(); } @Override public boolean isEmpty() { return array.isEmpty(); } public int getCapacity() { return array.getCapacity(); } @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 String toString() { StringBuffer res = new StringBuffer(); 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(); } public static void main(String[] args) { ArrayQueue<Integer> queue = new ArrayQueue<>(); for (int i = 0; i < 10; i++) { queue.enqueue(i); System.out.println(queue); if (i % 3 == 2) { queue.dequeue(); System.out.println(queue); } } } }
輸出結果:
Queue: front [0] tail Queue: front [0, 1] tail Queue: front [0, 1, 2] tail Queue: front [1, 2] tail Queue: front [1, 2, 3] tail Queue: front [1, 2, 3, 4] tail Queue: front [1, 2, 3, 4, 5] tail Queue: front [2, 3, 4, 5] tail Queue: front [2, 3, 4, 5, 6] tail Queue: front [2, 3, 4, 5, 6, 7] tail Queue: front [2, 3, 4, 5, 6, 7, 8] tail Queue: front [3, 4, 5, 6, 7, 8] tail Queue: front [3, 4, 5, 6, 7, 8, 9] tail
因爲基於咱們的動態數組Array來實現的棧,因此該隊列也具有了縮容和擴容的能力。
ArrayQueue<E>
數組隊列的出隊時間複雜度爲O(n),主要問題是由於出隊後,隊伍中的元素都要往前移動。
過程大體以下:
隊列:
出隊:
移動:
維護size:
咱們考慮實現一種循環隊列,記錄隊頭head指向和隊尾tail指向。這麼一來入隊和出隊只要分別向後移動tail和head一個位置便可。該問題就能夠簡化成以下方式的一種操做:
隨着入隊出隊的持續進行,爲了充分利用前方出隊後留下的空間,tail在7位置時,若是繼續入隊,tail將指向0位置推動,所謂循環隊列,就是這個意思。就像一個環形的傳送帶,只用移動頭尾標記,就能夠很方便地處理隊列元素。因此循環隊列在實現時要考慮按容量取模的處理狀況。
另外,再考慮循環隊列的另一種狀況:
關於循環隊列,在實現時,在head與tail處於相同位置的時候,咱們認爲隊列爲空:
隨着持續入隊出隊,在循環移動tail和front的過程當中,tail可能會追上front:
因爲tail==front不能即表達爲「隊列空」,又表達爲「隊列滿」。爲了解決這個問題,循環隊列有意浪費一個空間:
所以,tail == front表明隊列空,(tail + 1) % capacity = front表明隊列滿。
據此,咱們實現循環隊列以下:
public class LoopQueue<E> implements Queue<E> { private E[] data; private int front, tail; private int size; public LoopQueue(int capacity) { // capacity是用戶指望的存儲元素數量,實現隊列時要浪費一個空間,因此apacity + 1 data = (E[])new Object[capacity + 1]; front = 0; tail = 0; size = 0; } public LoopQueue() { this(10); } public int getCapacity() { return data.length - 1; } @Override public int getSize() { return size; } @Override public boolean isEmpty() { return front == tail; } @Override public void enqueue(E e) { if ((tail + 1) % data.length == front) { resize(getCapacity() * 2); } data[tail] = e; tail = (tail + 1) % data.length; size++; } // O(1) 相比數組隊列,由O(n)變成了O(1) @Override public E dequeue() { if (isEmpty()) { throw new IllegalArgumentException("Cannot dequeue from an empty queue."); } E ret = data[front]; data[front] = null; front = (front + 1) % data.length; size--; if (size == getCapacity() / 4 && getCapacity() /2 != 0) { resize(getCapacity() / 2); } return ret; } private void resize(int newCapacity) { E[] newData = (E[])new Object[newCapacity + 1]; for (int i = 0; i < size; i++) { // 將原來data中的元素放到newData中 // front做爲第一個元素,因此有front的誤差,因爲是循環隊列,因此要取模運算 newData[i] = data[(i + front) % data.length]; } data = newData; front = 0; tail = size; } @Override public E getFront() { if (isEmpty()) { throw new IllegalArgumentException("Cannot dequeue from an empty queue."); } return data[front]; } @Override public String toString() { StringBuilder res = new StringBuilder(); res.append(String.format("Queue: size = %d, capacity = %d\n", size, getCapacity())); res.append("front ["); for (int i = front; i != tail; i = (i + 1) % data.length) { res.append(data[i]); if ((i + 1) % data.length != tail) { res.append(", "); } } res.append("] tail"); return res.toString(); } public static void main(String[] args) { LoopQueue<Integer> queue = new LoopQueue<>(); for (int i = 0; i < 10; i++) { queue.enqueue(i); System.out.println(queue); if (i % 3 == 2) { queue.dequeue(); System.out.println(queue); } } } }
LoopQueue<E>
import java.util.Random; public class Main { // 測試使用q測試運行opCount個enqueue和dequeue操做所須要的時間,單位:秒 private static double testQueue(Queue<Integer> q, int opCount) { long startTime = System.currentTimeMillis(); Random random = new Random(); for (int i = 0; i < opCount; i++) { q.enqueue(random.nextInt(Integer.MAX_VALUE)); } for (int i = 0; i < opCount; i++) { q.dequeue(); } long endTime = System.currentTimeMillis(); return (endTime - startTime) / 1000.0; } public static void main(String[] args) { int opCount = 100000; ArrayQueue<Integer> arrayQueue = new ArrayQueue<>(); double time1 = testQueue(arrayQueue, opCount); System.out.println("ArrayQueue, time: " + time1 + " s"); LoopQueue<Integer> loopQueue = new LoopQueue<>(); double time2 = testQueue(loopQueue, opCount); System.out.println("LoopQueue, time: " + time2 + " s"); } }
測試輸出:
ArrayQueue, time: 3.895 s LoopQueue, time: 0.014 s