学习算法和数据结构可以帮忙我们编写更高效的程序,也可以帮忙我们在使用类库时选择合适的类和方法,带着这样的目的我们开始学习算法和数据结构。
算法分析
性能是算法研究的核心问题,也就是程序的运行时间,程序的运行时间主要和两点有关:
1.每条语句的执行时间
2.每条语句的执行频率
前者取决于不同计算机的性能,后者取决于程序本身和输入
例如我们分析下面这段程序的运行时间
总时间T(N)=(t1/6)N^3+(t2/2-t1/2)N^2+(t1/3-t2/2+t3)N+t4+t0x,得到的表达式很复杂,而且可以看出要得到精确的时间T(N)也很困难,因此算法分析时一般忽略与机器相关的常量,只关注时间增长的趋势。
上面这个例子T(N)去掉小项和常数,增长的数量级就是N^3,记做T(N)=O(N^3),也叫做渐进时间复杂度。官方的定义如下:
若存在函数f(N),使得当N趋近于无穷大时,T(N)/f(N)的极限值为不等于零的常数,则称f(N)是T(N)的同数量级函数。
记作 T(N)=O(f(N)),称O(f(N)) 为算法的渐进时间复杂度,简称时间复杂度。
渐进时间复杂度用大写O来表示,所以也被称为大O表示法。
常见的时间复杂度级别有
O(1):常量时间阶,O(n):线性时间阶,O(logn):对数时间阶,O(n*logn):线性对数时间阶,O (n^k):k≥2,k次方时间阶
其大小关系为:
O(1) < O(logn) < O(n) < O(n*logn) < O(n^2) < O(n^3)< O(2^n) < O(n!) < O(n^n)
而通常时间复杂度与运行时间有一些常见的比例关系:
复杂度 | 10 | 20 | 50 | 100 | 1000 | 10000 | 100000 |
O(1) | <1s | <1s | <1s | <1s | <1s | <1s | <1s |
O(log2(n)) | <1s | <1s | <1s | <1s | <1s | <1s | <1s |
O(n) | <1s | <1s | <1s | <1s | <1s | <1s | <1s |
O(n*log2(n)) | <1s | <1s | <1s | <1s | <1s | <1s | <1s |
O(n2) | <1s | <1s | <1s | <1s | <1s | 2s | 3-4 min |
O(n3) | <1s | <1s | <1s | <1s | 20s | 5 hours | 231 days |
O(2n) | <1s | <1s | 260 days | hangs | hangs | hangs | hangs |
O(n!) | <1s | hangs | hangs | hangs | hangs | hangs | hangs |
O(nn) | 3-4 min | hangs | hangs | hangs | hangs | hangs | hangs |
为了便于估算一个算法的时间复杂度,我们约定一下几条可操作的规则:
(1)读写单个常量或单个变量、赋值、算术运算、关系运算、逻辑运算等,计为一个单位时间。
(2)条件语句if(C){s},执行时间为(条件C的执行时间)+(语句块s的执行时间)。
(3)条件语句if(C)s1 else s2,执行时间为(条件C的执行时间)+(语句块s1和s2中执行时间最长的那个时间)。
(4)switch...case语句的执行时间是所有case子句中,执行时间最长的语句块。
(5)访问一个数据的单个元素或一个结构体变量的单个元素只需要一个单位时间。
(6)执行一个for循环语句需要的时间等于执行该循环体所需要时间乘上循环次数。
(7)执行一个while(C){s}循环语句或者执行一个do{s} while(C)语句,需要的时间等于计算条件表达式C的时间与执行循环s的时间之和再乘以循环的次数。
(8)对于嵌套结构,算法的时间复杂度由嵌套最深层语句的执行次数决定的。
(9)对于函数调用语句,它们需要的时间包括两部分,一部分用于实现控制转移,另一部分用于执行函数本身。
O(1)的例子 2数相加
c = a + b;
O(N)的例子 求数组中的最大元素
int max = a[0];
for(int i = 1; i < N; i++)
if(a[i] > max) max = a[i];
O(logN) 的例子 二分查找
二分查找的时间取决于while循环执行的次数,while循环每执行一次,需要查找的数组长度折半除以2, 因此while循环次数i与数组长度N的关系是N = 2^i, 所以时间增长的数量级是O(logN)
public static int rank(int key, int[] a) {
int lo = 0;
int hi = a.length - 1;
while (lo <= hi) {
// Key is in a[lo..hi] or not present.
int mid = lo + (hi - lo) / 2;
if (key < a[mid]) hi = mid - 1;
else if (key > a[mid]) lo = mid + 1;
else return mid;
}
return -1;
}
O(N^2)的例子 统计数组中2个数相加等于0的对数
for(int i = 1; i < N; i++)
for(int j = i+1; j < N; j++)
if (a[i] + a[j] == 0) cnt++;
抽象数据类型
数据类型是指一组值和一组对这些值的操作的集合,我们知道java原始的数据类型有int,float,double等,可以进行
+,-,*,/操作,原则上使用原始数据类型就可以了,但在更高层次的抽象上编写程序程序会更加方便。
抽象数据类型(ADT)是一种对使用者隐藏内部表示的数据类型,他将数据和操作数据的方法关联起来,并将数据的表示方式隐藏起来,使用者只需要通过API来使用抽象数据类型,不需要关心数据的具体表示方式和API的实现,当需要需要更换算法时,只需要修改API的实现,不需要修改任何用例代码。
java中使用class表示抽象数据类型,如果只使用基本数据类型我们在程序中只能做数值运算,有了class我们就可以在程序中操作字符串,图像,声音,日期等各种抽象类型。
后面学习各种算法和数据结构时,我们首先定义算法的class及API,然后实现API来描述对应的算法和数据结构。
背包/栈/队列
许多基础数据类型都和对象的集合有关,这些数据类型的值就是一组对象的集合,对这些数据类型的操作就是对集合中的对象进行增加删除和查询,这节我们学习3种对象集合类型的数据结构,这3种数据结构都可以通过数组或链表来实现,
数组是java支持的类型,它的优点是访问每个元素都是常数时间,缺点是需要预先指定数组的大小,当数组大小不够时需要重新调整数组大小,而且删除效率比较低,如果从指定位置删除一个元素需要把这个元素后面的所有元素往前移。
链表是非java直接支持的类型,它或者为空(null),或者由一个个的节点相互连接而成,每个结点含有一个泛型的元素和一个指向下一个节点引用。
我们可以用下面的方式表示一个节点
private class Node
{
Item item;
Node next;
}
Item是一个泛型类型,可以表示任意类型
下图创建了一个由3个节点组成的链表
下图演示了怎么从链表头部插入一个节点,一般会维护一个first引用指向头节点
下图演示了怎么从链表头部删除一个节点
下图演示了怎么从链表尾部插入一个节点,这里需要额外维护一个last引用指向尾节点
可以通过下面的方式遍历链表
for (Node x = first; x != null; x = x.next) {
// process x.item
}
背包实现
背包支持往集合添加元素,判断集合是否为空,判断集合元素个数以及遍历集合中元素,不支持删除元素
背包的API定义如下
public class Bag<Item> implements Iterable<Item>
---------------------------------------------------------------
public Bag(); //构造一个空背包
public boolean isEmpty(); //判断是否为空
public int size(); //大小
public void add(Item item); //添加元素
public Iterator<Item> iterator(); //遍历元素
集合类数据类型的特点是可以存储任意数据类型,所以我们用java泛型表示集合中的元素。
ArrayBag.java //数组实现背包,初始数组的大小为2,当背包满时调用resize增大数组的大小。
import java.util.Iterator;
import java.util.NoSuchElementException;
@SuppressWarnings("unchecked")
public class ArrayBag<Item> implements Iterable<Item> {
Item[] a;
int N;
public ArrayBag() {
a = (Item[]) new Object[2];
}
public boolean isEmpty() { return N == 0; }
public int size() { return N; }
private void resize(int capacity) {
assert capacity >= N;
Item[] temp = (Item[]) new Object[capacity];
for(int i = 0; i < N; i++)
temp[i] = a[i];
a = temp;
}
public void add(Item item) {
if (N == a.length) resize(a.length*2);
a[N++] = item;
}
@Override
public Iterator<Item> iterator() {
return new BagIterator();
}
private class BagIterator implements Iterator<Item> {
private int i = 0;
@Override
public boolean hasNext() {
return i < N;
}
@Override
public Item next() {
if (!hasNext()) throw new NoSuchElementException();
return a[i++];
}
}
}
Bag.java //链表实现背包
import java.util.Iterator;
import java.util.NoSuchElementException;
public class Bag<Item> implements Iterable<Item> {
private int N;
private Node first;
private class Node {
private Item item;
private Node next;
}
public Bag() {
N = 0;
first = null;
}
public boolean isEmpty() {
return first == null;
}
public int size() {
return N;
}
public void add(Item item) { //插入元素从链表的头部插入
Node oldFirst = first;
first = new Node();
first.item = item;
first.next = oldFirst;
N++;
}
/**
* Return an iterator that iterates over the items in the bag.
*/
public Iterator<Item> iterator() {
return new ListIterator();
}
private class ListIterator implements Iterator<Item> {
private Node current = first;
@Override
public boolean hasNext() {
return current != null;
}
@Override
public Item next() {
if (!hasNext()) throw new NoSuchElementException();
Item item = current.item;
current = current.next;
return item;
}
}
}
栈的实现
栈的特点是后进先出(LIFO),类似我们堆箱子,最上面的箱子最后堆,取的时候却是最先取,我们常用的函数的局部变量就存储在栈中,栈的API定义如下
public class Stack<Item> implements Iterable<Item>
------------------------------------------------------------------------------------
public Stack();
public boolean isEmpty();
public int size();
public void push(Item item); //栈顶插入元素
public Item pop(); //栈顶取元素
public Item peek(); //获取栈顶元素
public Iterator<Item> iterator();
ArrayStack.java //数组实现栈
import java.util.Iterator;
import java.util.NoSuchElementException;
@SuppressWarnings("unchecked")
public class ArrayStack<Item> implements Iterable<Item> {
Item[] a;
int N;
public ArrayStack() {
a = (Item[]) new Object[2];
}
public boolean isEmpty() { return N == 0; }
public int size() { return N; }
private void resize(int capacity) {
assert capacity >= N;
Item[] temp = (Item[]) new Object[capacity];
for(int i = 0; i < N; i++)
temp[i] = a[i];
a = temp;
}
public void push(Item item) {
if (N == a.length) resize(a.length*2); //满时扩大2倍数组长度
a[N++] = item;
}
public Item pop() {
if (isEmpty()) { throw new RuntimeException("Stack underflow error"); }
Item item = a[N-1];
a[N-1] = null; //防止对象游离,因为元素取出用完后应该由垃圾收集器收回,如果不赋值为null,数组就还在引用此元素,无法收回
N--;
if (N > 0 && N==a.length/4) resize(a.length/2); //当元素个数只有数组长度的1/4时,输小数组长度
return item;
}
public Item peek() {
if (isEmpty()) throw new NoSuchElementException("Stack underflow");
return a[N-1];
}
@Override
public Iterator<Item> iterator() {
return new LIFOIterator();
}
private class LIFOIterator implements Iterator<Item> {
private int i = N;
@Override
public boolean hasNext() {
return i > 0;
}
@Override
public Item next() {
if (!hasNext()) throw new NoSuchElementException();
return a[--i];
}
}
}
Stack.java //链表实现栈
import java.util.Iterator;
import java.util.NoSuchElementException;
public class Stack<Item> implements Iterable<Item> {
private int N;
private Node first;
private class Node {
private Item item;
private Node next;
}
public Stack() {
N = 0;
first = null;
}
public boolean isEmpty() {
return first == null;
}
public int size() {
return N;
}
public void push(Item item) {
Node oldFirst = first;
first = new Node();
first.item = item;
first.next = oldFirst;
N++;
}
public Item pop() {
if (isEmpty()) throw new NoSuchElementException("Stack underflow");
Item item = first.item;
first = first.next;
N--;
return item;
}
public Item peek() {
if (isEmpty()) throw new NoSuchElementException("Stack underflow");
return first.item;
}
/**
* Return an iterator that iterates over the items in the bag.
*/
public Iterator<Item> iterator() {
return new ListIterator();
}
private class ListIterator implements Iterator<Item> {
private Node current = first;
@Override
public boolean hasNext() {
return current != null;
}
@Override
public Item next() {
if (!hasNext()) throw new NoSuchElementException();
Item item = current.item;
current = current.next;
return item;
}
}
}
队列的实现
队列的特点是先进先出(FIFO),类似我们排队购物,先来的优先服务,队列的API定义如下:
public class Queue<Item> implements Iterable<Item>
--------------------------------------------------------
public Queue();
public boolean isEmpty();
public int size();
public void enqueue(Item item); //入队
public Item dequeue();
public Iterator<Item> iterator();
ArrayQueue.java //数组实现队列
import java.util.Iterator;
import java.util.NoSuchElementException;
@SuppressWarnings("unchecked")
public class ArrayQueue<Item> implements Iterable<Item> {
Item[] a;
int N;
int first; // index of first element of queue
int last; // index of next available slot
public ArrayQueue() {
a = (Item[]) new Object[2];
}
public boolean isEmpty() { return N == 0; }
public int size() { return N; }
private void resize(int capacity) {
assert capacity >= N;
Item[] temp = (Item[]) new Object[capacity];
for(int i = 0; i < N; i++)
temp[i] = a[(first + i) % a.length];
a = temp;
first = 0;
last = N;
}
public void enqueue(Item item) {
if (N == a.length) resize(a.length*2);
a[last++] = item;
if (last == a.length) last = 0; //队尾在数组中可能跑到队头前面去
N++;
}
public Item dequeue() {
if (isEmpty()) { throw new RuntimeException("Stack underflow error"); }
Item item = a[first];
a[first] = null;
first++;
if (first == a.length) first = 0;
N--;
if (N > 0 && N==a.length/4) resize(a.length/2);
return item;
}
@Override
public Iterator<Item> iterator() {
return new QueueIterator();
}
private class QueueIterator implements Iterator<Item> {
private int i = 0;
@Override
public boolean hasNext() {
return i < N;
}
@Override
public Item next() {
if (!hasNext()) throw new NoSuchElementException();
Item item = a[(i + first) % a.length]
i++;
return item;
}
}
}
Queue.java //链表实现队列
import java.util.Iterator;
import java.util.NoSuchElementException;
public class Queue<Item> implements Iterable<Item> {
private int N;
private Node first;
private Node last;
private class Node {
private Item item;
private Node next;
}
public Queue() {
N = 0;
first = null;
last = null;
}
public boolean isEmpty() {
return first == null;
}
public int size() {
return N;
}
public void enqueue(Item item) {
Node oldLast = last;
last = new Node();
last.item = item;
last.next = null;
if (isEmpty()) first = last;
else oldLast.next = last;
N++;
}
public Item dequeue() {
if (isEmpty()) throw new NoSuchElementException("Stack underflow");
Item item = first.item;
first = first.next;
N--;
if (isEmpty()) last = null; // to avoid loitering
return item;
}
/**
* Return an iterator that iterates over the items in the bag.
*/
public Iterator<Item> iterator() {
return new ListIterator();
}
private class ListIterator implements Iterator<Item> {
private Node current = first;
@Override
public boolean hasNext() {
return current != null;
}
@Override
public Item next() {
if (!hasNext()) throw new NoSuchElementException();
Item item = current.item;
current = current.next;
return item;
}
}
}