文章目录

  • 1 无序链表的顺序查找
    • 1.1 无序链表实现
    • 1.2 分析
  • 2 有序数组中的二分查找
    • 2.1 实现
    • 2.2 分析
    • 3 对二分查找的分析
  • 4 总结
  • 5 后记

1 无序链表的顺序查找

1.1 无序链表实现

符号表中使用的数据结构的一个简单选择就是链表,每个节点存储一个键值对。

  • get(K key):方法实现就是遍历链表,equals()方法比较节点中键的相等性。匹配成功,返回节点中对应的值;否则返回null;
  • put(K key, V value):方法也是遍历链表,equals()方法比较表中每个节点的键的相等性。匹配成功,用新值更新节点中对应的旧值;否则就构建新节点插入链表头。

具体实现如下代码1-1所示:

public class SequentialSearchST<K, V> extends AbstractST<K, V>{/*** 头结点*/private Node<K, V> first;/*** 符号表键值对数量*/private int size;private class Node<K, V> {K key;V val;Node<K, V> next;public Node(K key, V val, Node<K, V> next) {this.key = key;this.val = val;this.next = next;}}@Overridepublic void put(K key, V value) {for (Node<K, V> x = first; x != null; x = x.next) {if (x.key.equals(key)) {x.val = value;return;}}first = new Node<>(key, value, first);size++;}@Overridepublic V get(K key) {for (Node<K, V> x = first; x != null; x = x.next) {if (x.key.equals(key)) {return x.val;}}return null;}@Overridepublic V delete(K key) {Node<K, V> pre = null;Node<K, V> p = null;V oldVal = null;for (Node<K, V> x = first; p == null && x != null; x = x.next) {if (x.key.equals(key)) {if (pre == null) {p = first;first = first.next;} else {p = x;pre.next = x.next;}oldVal = p.val;p.val = null;p.next = null;size--;}pre = x;}return oldVal;}@Overridepublic int size() {return size;}@Overridepublic Iterable<K> keys() {Set<K> keySet = new HashSet<>();for (Node<K, V> x = first; x != null; x = x.next) {keySet.add(x.key);}return keySet;}@Overridepublic String toString() {if (size == 0) {return "{}";}StringBuilder sb = new StringBuilder();sb.append('{');for (Node<K, V> x = first; x != null; x = x.next) {sb.append(x.key + "=" + x.val);if (x.next != null) {sb.append(", ");}}sb.append("}");return sb.toString();}
}

我们把之前ST做成接口,代码如下1-2所示:

/*** 符号表接口* @author Administrator* @version 1.0* @description* @date 2022-11-11 20:16*/
public interface ST<K, V> {/*** 键值对存入符号表* @param key       键* @param value     值*/void put(K key, V value);/*** 获取键key对应的值* @param key   键* @return      值*/V get(K key);/*** 删除键对应的键值对* @param key   键*/V delete(K key);/*** 键是否包含在符号表中* @param key   键* @return      {@code true}如果键在符号表中;否则返回{@code false}*/default boolean contains(K key) {return get(key) != null;}/*** 符号表是否为空* @return  {@code true}符号表为空;否则返回{@code false}*/default boolean isEmpty() {return size() == 0;}/*** 符号表中键值对的数量* @return  键值对的数量*/int size();/*** 键的迭代器* @return  键的迭代器*/Iterable<K> keys();
}

SequentialSearchST类的父类保留,只有个空参构造。

  • 测试下基本功能,测试结果:

  • public class STTest {public static void main(String[] args) {ST<String, Integer> st = new SequentialSearchST<>();st.put("11", 4234);st.put("21", 2342);st.put("31", 132);st.put("41", 41);System.out.println(st);System.out.println(st.get("11"));System.out.println(st.get("51"));System.out.println(st.size());System.out.println(st.delete("31"));System.out.println(st.contains("41"));System.out.println(st);}
    }
    

    测试结果:

    {41=41, 31=132, 21=2342, 11=4234}
    4234
    null
    4
    132
    true
    {41=41, 21=2342, 11=4234}
    

1.2 分析

命题A:在含有N对键值对的基于(无序)链表的符号表中,未命中查找和插入都需要N次比较。命中的查找在最坏情况下需要N次比较。特别地,向一个空表中插入N个不同的键需要∼N22\sim\frac{N^2}{2}∼2N2​次比较。

推论:向一个空表中插入N个不同的键需要∼N22\sim\frac{N^2}{2}∼2N2​次比较。

查找一个已经存在的键并不需要N次比较。一种度量方法就是查找表中的每个键,并将总时间除以N。我们将它称为随机命中。尽管我们实现的符号表查找模式不是随机的,很容易得出随机命中所需的平均比较次数为∼N2\sim\frac{N}{2}∼2N​。

上述分析证明我们实现的基于链表的实现以及顺序查找是非常低效的,无法满足FrequencyCounter处理庞大输入问题的需求。比较的总次数和查找的总次数与插入次数的乘积成正比。

2 有序数组中的二分查找

完整的有序符号表API实现如下代码2-1所示:

2.1 实现

import java.util.Arrays;
import java.util.HashSet;
import java.util.NoSuchElementException;
import java.util.Set;/*** @author Administrator* @version 1.0* @description* @date 2022-11-12 19:33*/
public class BinarySearchST<K extends Comparable<K>, V> extends AbstractSortedST<K, V>{private K[] keys;private V[] vals;private int size;private int capacity;public BinarySearchST(int initCapacity) {if (initCapacity < 0) {throw new IllegalArgumentException("Illegal Capacity: "+capacity);}this.capacity = initCapacity;keys = (K[]) new Comparable[capacity];vals = (V[]) new Object[capacity];}@Overridepublic K min() {checkEmpty();return keys[0];}@Overridepublic K max() {checkEmpty();return keys[size - 1];}private void checkEmpty() {if (size == 0) {throw new NoSuchElementException("元素不存在");}}@Overridepublic K floor(K key) {int i = rank(key);if (i == 0 && keys[0].compareTo(key) != 0) {return null;}return keys[i];}@Overridepublic K ceiling(K key) {int i = rank(key);if (i == size) {return null;}return keys[i];}@Overridepublic int rank(K key) {int lo = 0, hi = size - 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;}@Overridepublic K select(int k) {checkIndex(k - 1);return keys[k];}@Overridepublic V delete(int index) {checkIndex(index);V oldVal = vals[index];for (int i = index; i < size; i++) {keys[i] = keys[i + 1];vals[i] = vals[i + 1];}size--;return oldVal;}private void checkIndex(int index) {if (index < 0 || index >= size) {throw new IndexOutOfBoundsException("索引越界");}}@Overridepublic V deleteMin() {return delete(0);}@Overridepublic V deleteMax() {return delete(size - 1);}@Overridepublic V delete(K key) {int k = rank(key);V oldVal = null;if (k < size && keys[k].compareTo(key) == 0) {// 删除oldVal = delete(k);}return oldVal;}@Overridepublic int size(K lo, K hi) {int l = rank(lo);int h = rank(hi);if (h < l) {throw new IllegalArgumentException("范围上限小于范围下限");}return contains(hi) ? h - l + 1: h - l;}@Overridepublic void put(K key, V value) {int k = rank(key);if (k < size && keys[k].compareTo(key) == 0) {vals[k] = value;return;}if (size == capacity) {throw new StackOverflowError("数组溢出");}for (int i = size - 1; i >= k; i--) {keys[i + 1] = keys[i];vals[i + 1] = vals[i];}keys[k] = key;vals[k] = value;size++;}@Overridepublic V get(K key) {int i = rank(key);if (i < size && keys[i].compareTo(key) == 0) {return vals[i];}return null;}@Overridepublic int size() {return size;}@Overridepublic Iterable<K> keys(K lo, K hi) {Set<K> keySet = new HashSet<>();int  h = rank(hi);for (int i = rank(lo); i < h; i++) {keySet.add(keys[i]);}if (contains(hi)) {keySet.add(keys[h]);}return keySet;}@Overridepublic Iterable<K> keys() {return new HashSet<>(Arrays.asList(keys));}@Overridepublic String toString() {if (size == 0) {return "{}";}StringBuilder sb = new StringBuilder();sb.append('{');for (int i = 0; i < size; i++) {sb.append(keys[i]).append("=").append(vals[i]);if (i != size - 1) {sb.append(", ");}}sb.append("}");return sb.toString();}
}

继承结构同无线链表一样,这样不在赘述,完整代码在文章最后仓库中。

2.2 分析

使用一对平行数组,一个Comparable类型的数组存存放键,put方法实现键的有序性;另外一个数组存储值。然后利用数组索引来高效实现get()和其他操作

实现的核心是rank()方法,它返回表中小于给定键的键的数量。

对于put()方法,如果给定的键存在于表中,那么就通过rank()方法可以精确定位索引,完成值的替换;否则键就不存在与表中,要插入的位置就是rank()方法返回的索引k。索引从[k,size-1]键值对数组同时完成整体的数据后移一位以腾出索引k处的位置存储新的键值对。

因为底层用数组实现,关于数组扩容或缩容问题可以参考ArrayList或其他基于数组实现的数据结构,这里暂时不做实现。

  • 注意事项

    • 索引越界问题
    • 空表取值问题
    • 满表存值问题,即有序数组表扩容问题

3 对二分查找的分析

命题B:在N个键的有序数组中进行二分查找最多需要(lg⁡N+1\lg N +1lgN+1)次比较(无论成功与否)

证明:可以参考排序分析

令C(N)C(N)C(N)为在大小N的符号表中查找一个键所需要进行的比较次数。显然C(0)=C(1)=0C(0)=C(1)=0C(0)=C(1)=0,且对于N>0N>0N>0我们可以写出一个和递归方法对于的归纳关系式:

C(N)≤C(⌊N2⌋)+1C(N)\le C(\lfloor\frac{N}{2}\rfloor)+1C(N)≤C(⌊2N​⌋)+1

无论查找是在左侧还是右侧,子数组的大小都不会超过⌊N2⌋\lfloor\frac{N}{2}\rfloor⌊2N​⌋,我们需要一次比较来检查中间元素和别查找元素是否相等,并决定是继续查找左侧还是右侧的子数组。当N为2的幂减1时(N=2n−1N=2^n-1N=2n−1),这种递推很容易。首先因为⌊N2⌋=2n−1−1\lfloor\frac{N}{2}\rfloor=2^{n-1}-1⌊2N​⌋=2n−1−1,所有:

C(2n−1)=C(2n−1)+1C(2^n-1)=C(2^{n-1})+1C(2n−1)=C(2n−1)+1

用这个公式代换上述不等式有:

C(2n−1)=C(2n−2)+1+1C(2^n-1)=C(2^{n-2})+1+1C(2n−1)=C(2n−2)+1+1

上述代换重复执行n-2次,得

C(2n−1)=C(20)+nC(2^n-1)=C(2^0)+nC(2n−1)=C(20)+n

最后结果:

C(N)=C(2n)≤n+1<lg⁡N+1C(N)=C(2^{n})\le n+1\lt \lg N + 1C(N)=C(2n)≤n+1<lgN+1

对于一般的N,确定结论更加复杂,单不难通过以上论证推广得到。二分查找所需时间必然在对数范围内。

上述实现中get(),ceiling(),floor()等只调用了一次rank(),2个参数的size调用了2次rank(),因此上述证明可以保证这些操作所需的时间都是对数级别。

尽量能够保证查找所需的时间是对数级别的,BinarySearchST仍然无法支持我们用类似FrequencyCounter的程序来处理大量问题,因为put()方法太慢了。二分查找减少了比较的次数但无法减少运行所需时间:在键随机排列情况下,构造一个基于有序数组的符号表所需要访问数组的次数是数组长度的平方级别(实际情况下键的排列虽然不是随机的,但仍然很好地符合这个模型。BinarySearchST的操作的成本表如下表3-1所示:

方法 运行所需时间的
的增长量级
put() 插入lg⁡N+N\lg N + NlgN+N ;更新 N
get() lg⁡N\lg NlgN
delete(int) N
delete(K) lg⁡N+N\lg N + NlgN+N
contains() lg⁡N\lg NlgN
size() 1
size(K,K) lg⁡N\lg NlgN
min() 1
max() 1
floor() lg⁡N\lg NlgN
ceiling() lg⁡N\lg NlgN
rank() lg⁡N\lg NlgN
select() 1
deleteMin() N
deleteMax() 1

命题C:向大小为N的有序数组中插入一个新的元素在最坏的情况下需要访问∼2N\sim 2N∼2N次数组,因此向一个空符号表中插入N个元素在最坏的情况下需要访问∼(N2+Nlg⁡N)\sim (N^2+N\lg N)∼(N2+NlgN)

证明:rank()+数组访问次数,即$ (\lg 1 + 2\cdot1)+(\lg2+2\cdot2)+…+(\lg N + 2\cdot N)=lg⁡(1⋅2⋯N)+2(1+2+⋯+N)=lg⁡N!+N(N+1)\lg(1\cdot2\cdots N) + 2(1+2+\cdots+N)=\lg N!+N(N+1)lg(1⋅2⋯N)+2(1+2+⋯+N)=lgN!+N(N+1)\sim (N^2+N\lg N)$

4 总结

下面看下符号表的初始实现的性能特点,如下表4-1所示:

算法(数据结构) 最坏情况下的成本
(N次插入后)
平均情况下的成本
(N次随机插入后)
算法高效的支持有序
性相关的操作
查找 插入 查找 插入
顺序查找(无序链表) N N N/2 N
二分查找(有序数组) lgN N+lgN lgN N+lgN

现在的问题是我们能否找到同时保证查找和插入都是对数级别的算法和数据结构。答案是可以。如果实现目标呢?要支持高效的插入操作,我们似乎需要一种链式结构,但链表无法使用二分查找法。为了将二分查找和链表灵活性结合起来,我们需要更加复杂的数据结构。能够同时拥有两者的就是二叉查找树,后面我们继续学习相关的知识。

5 后记

​ 如果小伙伴什么问题或者指教,欢迎交流。

❓QQ:806797785

⭐️源代码仓库地址:https://gitee.com/gaogzhen/algorithm

参考:

[1][美]Robert Sedgewich,[美]Kevin Wayne著;谢路云译.算法:第4版[M].北京:人民邮电出版社,2012.10

无序链表(顺序查找)和有序数组(二分查找)-基础实现-符号表(二)-数据结构和算法(Java)相关推荐

  1. 有序数组二分查找最接近的值

    有序数组二分查找最接近的值 问题 思路 代码 循环的终止条件 如何改为求upper_bound 问题 给你一个有序数组,数组里面有正有负,有重复值,再给你扔一个target 数,求这个数组中最接近ta ...

  2. 有序数组二分查找java_详解Java数据结构和算法(有序数组和二分查找)

    一.概述 有序数组中常常用到二分查找,能提高查找的速度.今天,我们用顺序查找和二分查找实现数组的增删改查. 二.有序数组的优缺点 优点:查找速度比无序数组快多了 缺点:插入时要按排序方式把后面的数据进 ...

  3. java数组二分查找_java 13-1 数组高级二分查找

    查找: 1.基本查找:数组元素无序(从头找到尾) 2.二分查找(折半查找):数组元素有序 pS:数组的元素必须有顺序,从小到大或者从大到小.以下的分析是从小到大的数组 二分查找分析: A:先对数组进行 ...

  4. 循环有序数组中查找某个数

    循环有序数组中查找某个数 利用二分查找,  一个循环有序数组从中间切分,一定分成一个有序数组和一个循环有序数组,由于循环有序数组不容易判断,我们只需判断哪一边是有序数组以及target是不是在有序数组 ...

  5. 有序数组中查找数字的范围

    有序数组中查找数字的范围 如题所示: 思路 源代码如下: 如题所示: 统计一个数字在排序数组中出现的次数 思路 使用二分查找,分别找到数字的下边界和上边界 源代码如下: class Solution ...

  6. 数组【数据结构与算法Java】

    数组[数据结构与算法Java] 数组 数组 略

  7. 【C语言】用二分查找法在有序数组中查找具体的某个数

    当给定一个有序数组,而我们要在这个数组中查找某个数时,我们常常用从头到尾的排除法遍历寻找,但是这种方式及其低效,n个数的数组最差要找n次.而后面我们会知道,二分查找法则高效的多,最差要找log以2为低 ...

  8. 用二分查找在一个有序数组中找数

    有序数组:a[]={1,2,3,4,5,6,7,8,9,10} 二分查找:首先在数组中找到中间位置的数组下标mid=(start+end)/2,将所要找的数x与mid进行比较: 若x>mid,则 ...

  9. this.$set 更新整个数组_学点算法(二)——有序数组二分搜索算法

    成功与失败 今天来学习一下二分搜索算法.二分搜索算法针对有序数组,如果数组乱序,则无法使用二分搜索法. 先来看一下二分搜索算法的运行原理: 判断区间是否有效,无效区间则退出循环. 取待查找区间的中间位 ...

最新文章

  1. Fastlane 入门实战教程从打包到上传iTunes connect
  2. Scala学习笔记-10
  3. 15天Python入门-3-流程控制-选择结构
  4. 将matpoltlib绘制好的图片从内存中取出
  5. [Qt教程] 第15篇 2D绘图(五)绘制图片
  6. reactor模型_Reactor模式以及Netty中的应用
  7. 南京理工大学计算机学院教师信息网,南京理工大学教师信息
  8. Mysql索引的类型
  9. Qt——P19 资源文件添加
  10. 关于《大道至简》第八章的收获
  11. MyBatis官方文档——Java API部分
  12. 手机屏碎了,怎样辨别是外屏坏还是内屏坏,看完这篇文章就明白了
  13. 基于MTCNN+arcface的人脸检测和人脸识别
  14. Amoeba实现mysql读写分离
  15. NLTK加载自定义词典
  16. 社会工程学工具集之钓鱼网站的制作
  17. recycle view设置item下划线
  18. 开放、数字化、创新、合作……华为云发力ing!
  19. Unity 游戏加载进度条的开发
  20. 第三次郑州.NET俱乐部活动总结

热门文章

  1. LaTeX 数学公式大全!
  2. Distraction-Aware Shadow Detection
  3. 1115: [POI2009]石子游戏Kam
  4. 信息学奥赛题解1168
  5. inf文件基础知识扫盲
  6. 最受欢迎的 Linux 怎么是它,Ubuntu 排第六
  7. 优雅代码 - 业务层代码命名
  8. 企业OA系统定制开发,挑战百日学习计划第50天(响应式网站开发)
  9. 思科模拟 实验十五:路由器的基本配置命令
  10. 计算机图形学--图像的两种存储方式:点阵存储和矢量存储