无序链表(顺序查找)和有序数组(二分查找)-基础实现-符号表(二)-数据结构和算法(Java)
创始人
2024-04-07 07:47:45
0

文章目录

    • 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 extends AbstractST{/*** 头结点*/private Node first;/*** 符号表键值对数量*/private int size;private class Node {K key;V val;Node next;public Node(K key, V val, Node next) {this.key = key;this.val = val;this.next = next;}}@Overridepublic void put(K key, V value) {for (Node 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 x = first; x != null; x = x.next) {if (x.key.equals(key)) {return x.val;}}return null;}@Overridepublic V delete(K key) {Node pre = null;Node p = null;V oldVal = null;for (Node 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 keys() {Set keySet = new HashSet<>();for (Node 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 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 {/*** 键值对存入符号表* @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 keys();
}

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

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

  • public class STTest {public static void main(String[] args) {ST 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, V> extends AbstractSortedST{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 keys(K lo, K hi) {Set 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 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

对于一般的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次随机插入后)
算法高效的支持有序
性相关的操作
查找插入查找插入
顺序查找(无序链表)NNN/2N
二分查找(有序数组)lgNN+lgNlgNN+lgN

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

5 后记

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

❓QQ:806797785

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

参考:

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

相关内容

热门资讯

喜欢穿一身黑的男生性格(喜欢穿... 今天百科达人给各位分享喜欢穿一身黑的男生性格的知识,其中也会对喜欢穿一身黑衣服的男人人好相处吗进行解...
发春是什么意思(思春和发春是什... 本篇文章极速百科给大家谈谈发春是什么意思,以及思春和发春是什么意思对应的知识点,希望对各位有所帮助,...
网络用语zl是什么意思(zl是... 今天给各位分享网络用语zl是什么意思的知识,其中也会对zl是啥意思是什么网络用语进行解释,如果能碰巧...
为什么酷狗音乐自己唱的歌不能下... 本篇文章极速百科小编给大家谈谈为什么酷狗音乐自己唱的歌不能下载到本地?,以及为什么酷狗下载的歌曲不是...
家里可以做假山养金鱼吗(假山能... 今天百科达人给各位分享家里可以做假山养金鱼吗的知识,其中也会对假山能放鱼缸里吗进行解释,如果能碰巧解...
华为下载未安装的文件去哪找(华... 今天百科达人给各位分享华为下载未安装的文件去哪找的知识,其中也会对华为下载未安装的文件去哪找到进行解...
四分五裂是什么生肖什么动物(四... 本篇文章极速百科小编给大家谈谈四分五裂是什么生肖什么动物,以及四分五裂打一生肖是什么对应的知识点,希...
怎么往应用助手里添加应用(应用... 今天百科达人给各位分享怎么往应用助手里添加应用的知识,其中也会对应用助手怎么添加微信进行解释,如果能...
客厅放八骏马摆件可以吗(家里摆... 今天给各位分享客厅放八骏马摆件可以吗的知识,其中也会对家里摆八骏马摆件好吗进行解释,如果能碰巧解决你...
苏州离哪个飞机场近(苏州离哪个... 本篇文章极速百科小编给大家谈谈苏州离哪个飞机场近,以及苏州离哪个飞机场近点对应的知识点,希望对各位有...