顺序查找与二分查找(Java实现)

首先给出需要用到的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;
	}
}

put()的过程图:

在这里插入图片描述
总结:

顺序查找,就是从第一个元素开始,按索引顺序遍历待查找序列,直到找出给定目标或者查找失败。

特点:

  • 对待查序列(表)无要求,即待查找序列可以是有序,也可以是无序;

  • 从第一个元素开始;

  • 需要逐一遍历整个待查序列(除非已经找到);

  • 若查找到最后一个元素还没找到,则查找失败;

缺点:

  • 效率低 – 需要遍历整个待查序列
  • 不支持有序性相关的操作

复杂度分析:

  • 时间复杂度: 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;
	}
}
put()方法图:

在这里插入图片描述

查找过程图:

在这里插入图片描述
总结:
二分查找,即我们先将需要被查找的键与子数组的的中间键进行比较,如果被查找的键小于中间键,我们就在左子数组中继续查找,如果被查找的键大于中间键,我们就在右子数组中继续查找,如果此中间键就是我们需要查找的值,则返回它。如果没有找到(即lo>hi时)则会返回最接近被查找键的子数组中间键。

特点:

  • 二分查找依赖数组结构

二分查找需要利用下标随机访问元素,如果我们想使用链表等其他数据结构则无法实现二分查找。

  • 二分查找针对的是有序数据

二分查找需要的数据必须是有序的。如果数据没有序,我们需要先排序,排序的时间复杂度最低是 O(nlogn)。(本文实现里使用了put方法来实现数组初始化即有序,且是遍历实现的,复杂度明显高于普通的排序算法)所以,如果我们针对的是一组静态的数据,没有频繁地插入、删除,我们可以进行一次排序,多次二分查找。这样排序的成本可被均摊,二分查找的边际成本就会比较低。

但是,如果我们的数据集合有频繁的插入和删除操作,要想用二分查找,要么每次插入、删除操作之后保证数据仍然有序,要么在每次二分查找之前都先进行排序。针对这种动态数据集合,无论哪种方法,维护有序的成本都是很高的。

所以,二分查找只能用在插入、删除操作不频繁,一次排序多次查找的场景中。针对动态变化的数据集合,二分查找将不再适用

  • 数据量太小不适合二分查找

如果要处理的数据量很小,完全没有必要用二分查找,顺序遍历就足够了。比如我们在一个大小为 10 的数组中查找一个元素,不管用二分查找还是顺序遍历,查找速度都差不多,只有数据量比较大的时候,二分查找的优势才会比较明显。

  • 数据量太大不适合二分查找

二分查找底层依赖的是数组,数组需要的是一段连续的存储空间,所以我们的数据比较大时,比如1GB,这时候可能不太适合使用二分查找,因为我们的内存都是离散的,可能电脑没有这么多的内存。

复杂度分析:

  • 时间复杂度:O(lgN)级别
  • 空间复杂度:O(N)级别
  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值