首先给出需要用到的API:
--------------------------------------------------------------------
顺序查找SequenceSearch
首先给出实现代码:
public class SequentialSearchST<Key, Value> {
private Node first;
private int N = 0;
private class Node {
Key key;
Value val;
Node next;
public Node(Key key, Value val, Node next) {
this.key = key;
this.val = val;
this.next = next;
}
}
public Value get(Key key) {
//查找就是遍历整个数组
for (Node x = first; x != null; x = x.next) {
if (key.equals(x.key)) {
return x.val;
}
}
return null;
}
public void put(Key key, Value val) {
//put方法里遍历整个链表,若找到key相同的键则更新其值
for (Node x = first; x != null; x = x.next) {
if (key.equals(x.key)) {
x.val = val;
return;
}
}
//否则在原节点之前创建一个新结点
first = new Node(key, val, first);
N++;
}
// Exercise 3.1.5
public int size() {
return N;
}
public void delete(Key key) {
first = delete(first, key);
}
private Node delete(Node x, Key key) {
if (x == null) {
return null;
}
if (x.key.equals(key)) {
N--;
return x.next;
}
x.next = delete(x.next, key);
return x;
}
public Iterable<Key> keys() {
Queue<Key> queue = new Queue<>();
for (Node x = first; x != null; x = x.next) {
queue.enqueue(x.key);
}
return queue;
}
}
总结:
顺序查找,就是从第一个元素开始,按索引顺序遍历待查找序列,直到找出给定目标或者查找失败。
特点:
-
对待查序列(表)无要求,即待查找序列可以是有序,也可以是无序;
-
从第一个元素开始;
-
需要逐一遍历整个待查序列(除非已经找到);
-
若查找到最后一个元素还没找到,则查找失败;
缺点:
- 效率低 – 需要遍历整个待查序列
- 不支持有序性相关的操作
复杂度分析:
- 时间复杂度: O(n),平均查找时间 = 列表长度/2
- 空间复杂度: O(n),1个待查序列+1个目标元素
------------------------------------------------------------------------------------
二分查找
public class BinarySearchST<Key extends Comparable<Key>, Value> {
private Key[] keys;
private Value[] vals;
private int N;
public BinarySearchST(int capacity) {
keys = (Key[]) new Comparable[capacity];
vals = (Value[]) new Object[capacity];
}
public int size() {
return N;
}
public Value get(Key key) {
if (isEmpty()) {
return null;
}
int i = rank(key);
if (i < N && keys[i].compareTo(key) == 0) {
return vals[i];
} else {
return null;
}
}
public boolean isEmpty() {
return N == 0;
}
public int rank(Key key) {
//这里我们可以看到在循环结束时,lo的值总是等于表中小于被查找的键的数量
int lo = 0, hi = N - 1;
while (lo <= hi) {
int mid = lo + (hi - lo) / 2;
int cmp = key.compareTo(keys[mid]);
if (cmp < 0) {
hi = mid - 1;
} else if (cmp > 0) {
lo = mid + 1;
} else {
return mid;
}
}
return lo;
}
public void put(Key key, Value val) {
//此put方法使数组初始化完毕后是以key值为依据有序的
//这里rank()返回的是小于key键的数量
int i = rank(key);
if (i < N && keys[i].compareTo(key) == 0) {
vals[i] = val;
return;
}
for (int j = N; j > i; j--) {
//在这里把比key大的键都向后移动一位
keys[j] = keys[j - 1];
vals[j] = vals[j - 1];
}
//插入key键
keys[i] = key;
vals[i] = val;
N++;
assert check();
}
/**
* Exercise 3.1.16
*
* @param key
*/
public void delete(Key key) {
if (isEmpty()) {
return;
}
int i = rank(key);
if (i < N && keys[i].compareTo(key) == 0) {
for (int j = i; j < N - 1; j++) {
keys[j] = keys[j + 1];
vals[j] = vals[j + 1];
}
N--;
keys[N] = null;
vals[N] = null;
}
assert check();
}
public Key min() {
return keys[0];
}
public Key max() {
return keys[N - 1];
}
public Key select(int k) {
return keys[k];
}
public Key ceiling(Key key) {
int i = rank(key);
return keys[i];
}
/**
* Exercise 3.1.17
*
* @param key
* @return
*/
public Key floor(Key key) {
int i = rank(key);
if (i < N) {
if (keys[i].compareTo(key) == 0) {
return key;
} else if (i > 0) {
return keys[i - 1];
}
}
return null;
}
public Iterable<Key> keys(Key lo, Key hi) {
Queue<Key> q = new Queue<Key>();
for (int i = rank(lo); i < rank(hi); i++) {
q.enqueue(keys[i]);
}
if (contains(hi)) {
q.enqueue(keys[rank(hi)]);
}
return q;
}
public boolean contains(Key key) {
return get(key) != null;
}
// Add for Exercise 3.1.29
public Iterable<Key> keys() {
return keys(min(), max());
}
public void deleteMin() {
delete(min());
}
public void deleteMax() {
delete(max());
}
// Exercise 3.1.30
private boolean check() {
return isSorted() && rankCheck();
}
private boolean isSorted() {
for (int i = 1; i < size(); i++) {
if (keys[i].compareTo(keys[i - 1]) < 0) {
return false;
}
}
return true;
}
private boolean rankCheck() {
for (int i = 0; i < size(); i++) {
if (i != rank(select(i))) {
return false;
}
}
for (int i = 0; i < size(); i++) {
if (keys[i].compareTo(select(rank(keys[i]))) != 0) {
return false;
}
}
return true;
}
}
总结:
二分查找,即我们先将需要被查找的键与子数组的的中间键进行比较,如果被查找的键小于中间键,我们就在左子数组中继续查找,如果被查找的键大于中间键,我们就在右子数组中继续查找,如果此中间键就是我们需要查找的值,则返回它。如果没有找到(即lo>hi时)则会返回最接近被查找键的子数组中间键。
特点:
- 二分查找依赖数组结构
二分查找需要利用下标随机访问元素,如果我们想使用链表等其他数据结构则无法实现二分查找。
- 二分查找针对的是有序数据
二分查找需要的数据必须是有序的。如果数据没有序,我们需要先排序,排序的时间复杂度最低是 O(nlogn)。(本文实现里使用了put方法来实现数组初始化即有序,且是遍历实现的,复杂度明显高于普通的排序算法)所以,如果我们针对的是一组静态的数据,没有频繁地插入、删除,我们可以进行一次排序,多次二分查找。这样排序的成本可被均摊,二分查找的边际成本就会比较低。
但是,如果我们的数据集合有频繁的插入和删除操作,要想用二分查找,要么每次插入、删除操作之后保证数据仍然有序,要么在每次二分查找之前都先进行排序。针对这种动态数据集合,无论哪种方法,维护有序的成本都是很高的。
所以,二分查找只能用在插入、删除操作不频繁,一次排序多次查找的场景中。针对动态变化的数据集合,二分查找将不再适用
- 数据量太小不适合二分查找
如果要处理的数据量很小,完全没有必要用二分查找,顺序遍历就足够了。比如我们在一个大小为 10 的数组中查找一个元素,不管用二分查找还是顺序遍历,查找速度都差不多,只有数据量比较大的时候,二分查找的优势才会比较明显。
- 数据量太大不适合二分查找
二分查找底层依赖的是数组,数组需要的是一段连续的存储空间,所以我们的数据比较大时,比如1GB,这时候可能不太适合使用二分查找,因为我们的内存都是离散的,可能电脑没有这么多的内存。
复杂度分析:
- 时间复杂度:O(lgN)级别
- 空间复杂度:O(N)级别