查找算法(上)

521 阅读4分钟

简介

健值对在现代计算机和网络系统应用广泛(常见如数据库系统、搜索引擎等),如果不能快速完成相关操作,大规模应用将无从谈起。 下面列举了健值对部分基础操作:

  • 查找 Value get(Key key)
  • 插入 void put(Key key, Value value)
  • 删除 void delete(Key key)
  • 包含检测 boolean contains(Key key)
  • 空检测 boolean isEmpty()
  • 大小 int size() 本文尝试找着一种实现方式,使得所有的操作都具备对数级别的时间复杂度,本文只关注查找和插入操作(因为其他接口差不多是这两个的封装)。

链表

链表实现是比较容易想到和理解的方式:将所有元素连接成链表:

查找和插入

因为是链表,所以就是遍历:

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) {
  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++;
}

优缺点

  • 优点:新增一个节点的时间是固定的
  • 缺点:插入、查找操作都是线性级别的时间复杂度,运气不好的话需要遍历整个链表

有序数组

针对链表查找慢的缺点,我们想到使用二分查找去解决,即使用一对平行数组:一个存储健,一个存储值,保证存放健的数组是有序的,再通过数组的索引去获取、更新值的数组:

查找和插入

因为是有序数组,所以使用二分搜索确定位置,插入新元素的同时,后移后面的元素,并适时增加数组长度:

public Value get(Key key) {
  if (isEmpty()) return null;
  int i = rank(key); 
  if (i < n && keys[i].compareTo(key) == 0)
    return vals[i];
  return null;
}

public void put(Key key, Value val)  {
  int i = rank(key);
  if (i < n && keys[i].compareTo(key) == 0) {
    vals[i] = val;
    return;
  }

  if (n == keys.length) resize(2*keys.length);

  // !!!后移健值
  for (int j = n; j > i; j--)  {
    keys[j] = keys[j-1];
    vals[j] = vals[j-1];
  }
  keys[i] = key;
  vals[i] = val;
  n++;
}

// 二分搜索找到key在数组中的index
public int rank(Key key) {
  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;
}

优缺点

  • 优点:检索复杂度达到对数级别。
  • 缺点:插入操作需要线性级别的时间复杂度,运气不好的话需要移动整个数组。

二叉查找树

本节我们介绍二叉查找树,一种结合链表和有序数组优点的数据结构:

如图所示,二叉查找树由一些节点链接而成,每个节点保存了健值、父结点(根结点除外)以及左、右两个链接(分别指向了左、右结点)。 为了保证快速搜索,二叉树的节点是有序的:每一个节点的健大于其左子树中的所有节点的健,小于其右子树中所有节点的健

查找与插入

因为节点是有序的,所以只需要比大小就能判断是当前节点还是在左右子树中,递归即可:

private Value get(Node x, Key key) {
  int cmp = key.compareTo(x.key);
  if      (cmp < 0) return get(x.left, key);
  else if (cmp > 0) return get(x.right, key);
  else              return x.val;
}

public void put(Key key, Value value) {
  root = put(root, key, value);
}

private Node put(Node curr, Key key, Value value) {
  // 遇到空节点,返回一个新建节点
  if (curr == null) 
    return new Node(key, value);

  int r = key.compareTo(curr.key);
  if (r < 0)
     curr.left = put(curr.left, key, value);
  else if (r > 0)
     curr.right = put(curr.right, key, value);
  else
     curr.value = value;

  return curr;
}

优缺点

由代码可知:二叉树形状受节点的插入顺序相关,下图分别展示了最优情况、一般情况以及最差情况:

显而易见,二叉树的最差运行时间取决于树的高度,而树的高度又是不可控的,其原因是二叉树缺乏调整节点位置的能力(只要确定键的大小,那么插入位置就确定了)

总结

文本层层递进,先后学习使用链表、有序数组和二叉树3种实现方式,但都不能保证最差情况下对数级别时间复杂度的要求,下篇文章 我们将在二叉树的基础上继续探索。