ywrby

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

原文链接

符号表

符号表最主要的目的就是将键与值联系起来

定义:符号表是一种储存键值对的数据结构,支持两种操作:插入(put),即将一组新的键值对存入表中;查找(get),即根据提供的键得到相应的值

API

返回值 名称 作用
构造函数 ST() 创建一张符号表
void put(Key key,Value val) 将键值对放入表中(若为空值,则删除这个键值对)
Value get(Key key) 获取键key对应的值,若不存在则返回null值
void delete(Key key) 删除键key对应的键值对
boolean contains(Key key) 判断key是否存在于表中
boolean isEmpty() 表是否为空
int size() 表中键值对数量
Iterable keys() 表中所有键的集合

关于符号表的一些规定

  1. 采用泛型构造符号表有利于扩展用例
  2. 对于重复的键我们规定每个键值对应一个值,当向符号表中存入已有的值时会将新值覆盖旧值
  3. 禁止空键的产生
  4. 同样禁止空值的产生,一旦赋予空值,按时删除这个键值对

查找的成本模型

成本模型的计算,通过统计比较次数(等价性测试,或是键的相互比较)。在内循环不进行比较(极少)的情况下,通过统计访问数组的次数来确定成本模型

实现二分查找(基于有序数组)

package cn.ywrby.Searches;

//二分查找,基于有序数组


import edu.princeton.cs.algs4.Queue;
import edu.princeton.cs.algs4.StdIn;
import edu.princeton.cs.algs4.StdOut;

import java.util.NoSuchElementException;

public class BinarySearchST<Key extends Comparable<Key>,Value> {
    private static final int INIT_CAPACITY=2;  //定义数组的初始大小
    private Key[] keys;  //键数组
    private Value[] vals;  //值数组
    private int n=0 ;  //数组大小
    //无参构造函数
    public BinarySearchST(){
        this(INIT_CAPACITY);  //利用固定初始大小调用含餐构造函数
    }
    //含参构造函数
    public BinarySearchST(int capacity){
        keys=(Key[]) new Comparable[capacity];
        vals=(Value[])new Object[capacity];
    }
    //动态调整数组大小
    private void resize(int capacity){
        assert capacity>=n:"不可以动态将数组调大";  //判断传入容量大小,不可以将容量调大。只通过put增加数组大小
        //assert [boolean 表达式] : [错误表达式 (日志)]  返回一个报错
        Key[] tempKey=(Key[]) new Comparable[capacity];
        Value[] tempValue=(Value[]) new Object[capacity];
        for(int i=0;i<n;i++){  //逐个复制放入新数组
            tempKey[i]=keys[i];
            tempValue[i]=vals[i];
        }
        vals=tempValue;
        keys=tempKey;
    }
    public int size(){return n;}
    public boolean isEmpty(){return size()==0;}
    //判断键是否存在
    public boolean contains(Key key) {
        //如果键为null即报错,警告键不可能为null
        if (key == null) throw new IllegalArgumentException("argument to contains() is null");
        return get(key) != null;
    }
    public Value get(Key key){
        if (key == null) throw new IllegalArgumentException("argument to get() is null");  //防止搜索null键
        if(isEmpty()) return null;   //防止搜索空数组
        int i=rank(key);  //获取对应键的索引值
        if(i<n && keys[i].compareTo(key)==0) return vals[i];  //返回对应值
        return null;  //未找到
    }
    /*
     *查找目标键的值
     * 如果存在,返回的对应键的索引值
     *如果不存在,返回表中小于它的键的数量(即新键的位置)
     * */
    public int rank(Key key){
        if(key==null) throw new IllegalArgumentException("argument to rank() is null");  //避免索引null值
        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;   //while循环结束条件是lo>hi此时lo的值即为小于key的键的数量
    }
    //插入元素
    public void put(Key key,Value val){
        if(key==null) throw new IllegalArgumentException("first argument to put() is null");
        //如果值为空,直接进行删除
        if(val==null){
            delete(key);
            return;
        }
        int i=rank(key);     //获取索引值
        //如果键已经存在
        if(i<n&&keys[i].compareTo(key)==0){
            vals[i]=val;
            return;
        }
        //如果键不存在
        //判断是否需要动态调整数组大小
        if(n==keys.length){
            resize(keys.length*2);
        }
        //将值按键序放入
        for(int j=n;j>i;j--){
            keys[j]=keys[j-1];
            vals[j]=vals[j-1];
        }
        keys[i]=key;
        vals[i]=val;
        n++;
        assert check();

    }

    //删除对应键的元素
    public void delete(Key key){
        if (key == null) throw new IllegalArgumentException("argument to delete() is null");
        if (isEmpty()) return;
        int i=rank(key);
        //不存在对应键
        if(i==n || keys[i].compareTo(key)!=0 ) return;
        //存在对应键
        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;
        //删减后动态调整数组大小
        if (n > 0 && n == keys.length/4) resize(keys.length/2);
        assert check();
    }

    //删除最小元素
    public void deleteMin() {
        if (isEmpty()) throw new NoSuchElementException("Symbol table underflow error");
        delete(min());
    }
    //删除最大元素
    public void deleteMax() {
        if (isEmpty()) throw new NoSuchElementException("Symbol table underflow error");
        delete(max());
    }
    //返回数组最小值
    public Key min() {
        if (isEmpty()) throw new NoSuchElementException("called min() with empty symbol table");
        return keys[0];
    }
    //返回数组最大值
    public Key max() {
        if (isEmpty()) throw new NoSuchElementException("called max() with empty symbol table");
        return keys[n-1];
    }
    //返回数组第k个键
    public Key select(int k) {
        if (k < 0 || k >= size()) {
            throw new IllegalArgumentException("called select() with invalid argument: " + k);
        }
        return keys[k];
    }


    //向下取整,找出小于等于该键的最大键
    public Key floor(Key key) {
        if (key == null) throw new IllegalArgumentException("argument to floor() is null");
        int i = rank(key);
        if (i < n && key.compareTo(keys[i]) == 0) return keys[i];
        if (i == 0) return null;
        else return keys[i-1];
    }
    //向上取整,找出大于等于该键的最小键
    public Key ceiling(Key key) {
        if (key == null) throw new IllegalArgumentException("argument to ceiling() is null");
        int i = rank(key);
        if (i == n) return null;
        else return keys[i];
    }


    //返回表中所有键的集合(已排序)
    public Iterable<Key> keys() {
        return keys(min(), max());
    }
    //返回表中从lo至hi的所有键的集合
    public Iterable<Key> keys(Key lo, Key hi) {
        if (lo == null) throw new IllegalArgumentException("first argument to keys() is null");
        if (hi == null) throw new IllegalArgumentException("second argument to keys() is null");

        Queue<Key> queue = new Queue<Key>();
        if (lo.compareTo(hi) > 0) return queue;
        for (int i = rank(lo); i < rank(hi); i++)
            queue.enqueue(keys[i]);
        if (contains(hi)) queue.enqueue(keys[rank(hi)]);
        return queue;
    }
    //检查数组是否符合规定条件
    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;
    }
    public static void main(String[] args) {
        BinarySearchST<String, Integer> st = new BinarySearchST<String, Integer>();
        for (int i = 0; !StdIn.isEmpty(); i++) {
            String key = StdIn.readString();
            st.put(key, i);
        }
        for (String s : st.keys())
            StdOut.println(s + " " + st.get(s));
    }
}

上述实现中,rank方法可以利用递归实现,更容易理解函数调用过程

public int rank(Key key,int lo,int hi){
    if(hi<lo) return lo;
    int mid=lo+(hi-lo)/2;
    int cmp=key.compareTo(keys[mid]);
    if(cmp<0) return rank(key,lo,mid-1);
    else if(cmp>0)return rank(key,mid+1,hi);
    else(return mid;)
}

算法分析

在N个键的有序数组中进行二分查找最多需要(lgN+1)次比较(无论是否成功)

显然易知C(0)=0,C(1)=1,且对于N>0可以利用递归方式归纳得知

C(N)=C([N/2]_{向下取整})+1

特殊的,当N=2^n-1时

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

利用上式不断替换得到

C(2^n-1)<=C(2^0)+n

易得

C(N)<=n+1<lgN+1

对于其他一般形式也可得类似结果

二分查找很好的解决了查找元素的问题,但没有解决插入元素的难题,向有序数组中插入元素,最坏情况下需要访问数组~2N次,因此向一个空数组中插入N个元素,最坏情况需要N^2次访问。
posted on 2020-04-05 18:49  ywrby  阅读(275)  评论(0编辑  收藏  举报