数据结构与算法——无序链表和有序数组二分查找

一、开篇说明

       离写上一篇博客已经隔了半个月了。今天要输出的内容是查找算法。

二、算法及其概念

2.1 无序链表的顺序查找

       链表这个算法结构在之前已经提到很多次了,他其实是数组的另一个重要的替代方式。链表就是一连串的节点,每个节点本身存储着一个键值对,同时也存储着一个引用或者说索引,它指向下一个节点的内存地址,但是要注意每个节点都是游离的,并不是连续的内存地址。无序链表指的是链表这个数据结构里存储的对象是无序的。通过无序链表实现查找只能沿着节点依次顺序的往下查找,然后判断每个节点key值是否与目标key值相等,相等则返回key所对应value,如果查找到最后一个即它的next索引为空了,还没有找到相同的key值,说明不存在,返回null。

2.1.1 算法实现

/**
 * 无序链表的顺序查找
 * @param <Key> 键
 * @param <Value> 值
 */
public class SequentialSearchST<Key, Value> {
    //第一个节点
    private Node first;

    private int n;

    //链表的定义
    private class Node {
        private Key key;
        Value value;
        Node next;

        public Node(Key key, Value value, Node next) {
            this.key = key;
            this.value = value;
            this.next = next;
        }
    }

    /**
     * 查找value
     * @param key
     * @return
     */
    public Value get(Key key) {
        for (Node x = first; x != null; x = x.next) {
            if (key.equals(x.key))
                return x.value;
        }
        return null;
    }

    /**
     * 存储key,value键值对,如果key值相同则覆盖
     * @param key
     * @param value
     */
    public void put(Key key, Value value) {
        for (Node x = first; x != null; x = x.next) {
            if (key.equals(x.key)) {
                x.value = value;
                return;
            }
        }
        Node newFirst = new Node(key, value, first);
        first = newFirst;
        n++;
    }

    public int size() {
        return n;
    }

    /**
     * 删除操作
     * @param key
     */
    public void delete(Key key) {
        //判断第一个key是否为目标key相同
        //相同则直接返回,否则进入for循环
        if (first.key.equals(key)) {
            first = first.next;
            n--;
            return;
        }
        //for循环从第二个节点开始进行判断
        for (Node x = first; x != null; x = x.next) {
            Node next = x.next;
            if (next != null && key.equals(next.key)) {
                x.next = next.next;
                n--;
                return;
            }

        }
    }

    /**
     * 返回一个迭代器
     * @return
     */
    public Iterable<Key> keys() {
        return new KeysIterable();
    }

    /**
     * 实现一个迭代器
     */
    class KeysIterable implements Iterable<Key> {

        Node current = first;

        @Override
        public Iterator iterator() {

            return new Iterator() {
                @Override
                public boolean hasNext() {
                    return current != null;
                }

                @Override
                public Key next() {
                    Node node = current;
                    current = current.next;
                    return node.key;
                }
            };
        }
    }

}

2.1.2 算法分析:

SequentialSearchST类维护了两个成员变量first和n,分别用来描述指向链表第一个节点的引用和链表的长度,其次还维护了一个内部类Node,它用来描述链表的节点,有三个属性key,value和next分别描述键,值和指向下一个节点内存地址的引用。

put(Key key, Value value)方法:

put()方法体先是一个for循环,它是遍历链表顺序查找是否包含相同的key值,如果命中相同的key,那就直接替换该key值对应value值并直接返回结束。否则创建新的节点将它赋值给first成员变量,并将新的first.next指向原来的first对象,这样就存入了新键值对,并存储在链表的第一个位置。偷个小懒,找了一副插入过程的分析轨迹截个大家,它所对应的测试类在下面

    @org.junit.Test
    public void testSequential() {
        SequentialSearchST<String, Integer> st = new SequentialSearchST<>();
        String[] a = {"S","E","A","R","C","H","E","X","A","M","P","L","E","Z"};
        for (int i = 0; i < a.length; i++) {
            st.put(a[i], i);
        }
        System.out.println(st.get("E"));
    }

get(Key key)方法:

get()方法很简单,遍历整个链表顺序查找含有与目标key相同key的node节点,找到直接返回value值,否则直接返回null。

delete(Key key)方法:

delete()方法相对来说复杂一点,先是判断第一个节点first的key是否与目标key相等,如果相等就意味着命中,直接把first.next赋值给first,n-1完事(原来的first对象就处于游离的状态,会被回收,因为现在first引用指向的是新的节点);否则遍历链表顺序查找与目标key相同的节点,注意此时for循环里面要对比的节点是从第二个节点开始,找到了目标key以后处理的逻辑就跟上面一样。

size()方法就直接跳过吧!!

迭代器:就是一个实现了next()和hasNext()方法的类的对象,实现迭代器的关键就是Node类的next成员变量,不停地找下一个节点,直到为next为null

2.2 有序数组中的二分查找

2.2.1 基本思想

它使用的数据结构是两个平行的数组分别存储键和值,即如果两个数组下标相就意味着他两存储的就是一个键值对。不难看出这两个数组的大小始终相等。而实现查找和存储数据的真正核心是rank()方法(后面会详细分析),如果查找的目标在数组中,它可以快速找到目标key所对应的数组下标(即索引)并返回它。几乎所有其他的方法实现都需要使用它。对于get()方法,rank()返回的索引可以快速找到value值返回;对put()方法,可以快速确定是否包含相同的key,以作后续处理;对于delete(),快速定位目标自不必说。

2.2.2 算法实现

/**
 * 有序数组中二分查找
 * @param <Key>
 * @param <Value>
 */
public class BinarySearchST<Key extends Comparable<Key>, Value> {
    private Key[] keys;
    private Value[] values;
    private int n;

    public BinarySearchST(int capacity) {
        keys = (Key[]) new Comparable[capacity];
        values = (Value[]) new Object[capacity];
    }

    public int size() {
        return n;
    }

    public boolean isEmpty() {
        return n==0;
    }

    /**
     * 根据key获取value值
     * @param key key
     * @return
     */
    public Value get(Key key) {
        if (isEmpty())
            return null;
        int i = rank(key);
        if (i < n && keys[i].compareTo(key) == 0)
            return values[i];
        else
            return null;

    }

    /**
     * 插入键值对
     * @param key
     * @param value
     */
    public void put(Key key, Value value) {
        int i = rank(key);
        if (i<n && keys[i].compareTo(key) == 0) {
            values[i] = value;
            return;
        }
        for (int j = n; j > i; j--) {
            keys[j] = keys[j-1];
            values[j] = values[j-1];
        }
        keys[i] = key;
        values[i] = value;
        n++;
    }

    /**
     * while循环实现二分查找算法
     * @param key
     * @return
     */
    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;
    }

    /**
     * 递归实现二分查找
     * @param key
     * @param lo
     * @param hi
     * @return
     */
    public int rank(Key key, int lo, int hi) {
        if (lo > hi)
            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;
    }

    /**
     * 删除一个元素
     * @param key
     */
    public void delete(Key key) {
        int i = rank(key);
        if (i < n && keys[i].compareTo(key) == 0) {
            for (int j = i; j < n; j++) {
                keys[j]=keys[j+1];
                values[j] = values[j+1];
            }
            keys[n] = null;
            keys[n] = null;
            n--;
        }
    }

    /**
     * 判断是否包含目标key
     * @param k
     * @return
     */
    public boolean contains(Key k) {
        int i = rank(k);
        if (k.compareTo(keys[i]) == 0)
            return true;
        return false;
    }

    /**
     * 迭代器,遍历链表
     * @return
     */
    public Iterable<Key> keys() {
        return keys(keys[0], keys[n-1]);
    }

    /**
     * 使用队列实现的迭代器
     * @param lo
     * @param hi
     * @return
     */
    public Iterable<Key> keys(Key lo, Key hi) {
        Queue<Key> queue = new Queue<>();
        for (int i = rank(lo); i < rank(hi); i++) {
            queue.enqueue(keys[i]);
        }
        if (contains(hi))
            queue.enqueue(hi);
        return queue;
    }

    public Key select(int i) {
        return keys[i];
    }

}

2.2.2 算法分析

首先想说的是这个泛型类BinarySearchST<Key extends Comparable<Key>, Value>,Key继承了Comparable<Key>,看起来这泛型Key好像没有什么限制,但是仔细分析它其实隐藏了一个限制,就是这个具体的泛型必须实现了Comparable接口,即这个类必须实现compareTo方法,jdk本身有很多类(如包装类型Integer等,还包括文件相关类File等)实现了这个接口,但是如果自己定义的类想使用这个二分查找算法就必须自己去实现Comparable接口的compareTo方法。


rank()方法:

这里我实现了两个rank()方法,一个是通过while循环实现,另一个是通过递归实现。先看while循环实现的rank()方法,它接受一个key参数,定义了两个局部变量lo和hi,初始化时可以把它们理解为数组的第一个索引和最后一索引,while循环结束的条件就是这两个索值引相等或hi小于lo,while循环体先计算这两个索引值的中位数mid,然后将key和Key数组中下标为mid的值比较。如果这个比较的值cmp小于0,意味着这个key在Key数组的lo到mid-1之间,然后将mid-1赋值给hi;如果cmp>0,key就在mid+1到hi之间,然后将mid+1赋值给lo。如果满足循环条件,则继续循环,直到找到目标值则返回目标索引或者未命中目标跳出循环直接返回lo值。值得读者注意的是这个索引不一定是命中目标的索引,如果是未命中目标的索引,返回的这个索引值是大于目标key的索引值的,因为在最后一轮hi-lo一定是等于1,所以mid=lo+(hi-lo)/2=lo,而cmp>0,故最终返回的lo=mid+1,也就大于目标key的索引位置(这里根据各种情况可能有所不同,但是要知道最后返回的索引是目标key之后的那个大索引)。

希望读者能仔细研究这个过程,因为它是该算法的核心。rank()递归方法跟while循环其实执行逻辑都差不多,只是实现方式不同,读者可以根据代码自行研究一番。


put()方法:

它分为两种情况,一种是存在相同的值,另一种是不存在相同的值。首先调用rank(),拿到一个索引,判断它所对应的key是否与目标相同,如果是则替换key的value值。否则for循环将索引i以后的key-value往后移动一位(注意这里没有实现数组动态扩缩容),随后将新的键值对添加进来。

get()方法:

get()方法也是先调用rank()方法,拿到索引值,然后判断key是否为目标key,如果是就返回value值;不是直接返回null。

delete()方法:

还是一样调用rank()拿到数组索引值,判断是否与目标key相同,如果不同说明链表中不存在这个key,结束;否则将索引位置i开始以后的索引值向前移动一位,最后将两个数组索引为n的位置赋值为null(注意这个n是数组包含元素的个数,不是数组的大小)。

最后说一下迭代器,这里使用我之前写的队列实现的迭代器。想看的同志可以点 这里

三、总结

在博客开始处说了这两个算法效率并不高,链表顺序查找算法的插入和查找都是线性级别的;有序数组二分查找算法的查找效率提高到对数级别,但是插入操作的效率还是线性级别的。

到这里就告一段落了,后续会继续带来二叉树查找和红黑二叉查找树。

 

参考资料:算法第四版

 

    不忘初心,死抠细节。仅以此博献给我伟大的java语言,如有不当之处,欢迎大神指正。谢谢!!!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值