一般用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