数据结构-查找篇

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


一、查找的基本概念

  • 列表:由同一类型的数据元素(或记录)构成的集合,可利用任意数据结构实现。
  • 关键字:数据元素的某个数据项的值,用它可以标识列表中的一个或一组数据元素。
  • 主关键字:如果一个关键字可以唯一标识列表中的一个数据元素,则称其为主关键字,否则为次关键字。当数据元素仅有一个数据项时,数据元素的值就是关键字。
  • 查找:根据给定的关键字值,在特定的列表中确定一个其关键字与给定值相同的数据元素,并返回该数据元素在列表中的位置。

在查找算法中要用到三类参量,即:

①查找对象K(找什么)
②查找范围L(在哪找)
③查找的结果(K在L中的位置)

其中① ②为输入参量,在函数中不可缺少。③为输出参量,可用函数返回值表示。
  • 平均查找长度:为确定数据元素在列表中的位置,需和给定值进行比较的关键字个数的期望值,称为查找算法在查找成功时的平均查找长度。

二、查找的几类算法

1.基于线性表的查找方法

1.1 顺序查找法
  • 顺序查找法的特点是:用所给关键字与线性表中各元素的关键字逐个比较,直到成功或失败。
  • 算法思想:从表的一端开始扫描,结束条件为:1.找到元素,2.到达表的另一端,且未找到。
  • 算法分析:用平均查找长度(ASL)分析顺序查找算法的性能。假设列表长度为n,那么查找第 i个数据元素时需进行 n-i+1 次比较,即 C=n-i+1。又假设查找每个数据元素的概率相等,即 P=1/n,则顺序查找算法查找成功时的平均查找长度为:
    在这里插入图片描述
1.2折半查找法
  • 条件:要求待查找的列表必须是按关键字大小有序排列的顺序表。

  • 基本过程:

     1、将表中间位置记录的关键字与查找关键字比较,如果两者相等,则查找成功;
     2、否则利用中间记录将表分成前、后两个子表,如果中间记录的关键字大于查找关键字,则进一步查找前一子表,否则进一步查找后一子表。
     3、重复以上过程,直到找到满足条件的记录,查找成功,或直到子表不存在为止,此时查找不成功。
    

    折半查找

  • 代码实现:

    public static int search(int[] array,int key) {
        int low = 0;
        int high = array.length-1;
        
        while (low <= high){
            int mid = (low+high)/2;
            
            if (array[mid] == key){
                //已找到,返回下标
                return mid;
            }else if (array[mid] > key){
                //key 应该在左边
                high = mid-1;
            }else {
                //key 应该在右边
                low = mid+1;
            }
        }
        //未找到,返回 -1
        return -1;
    }
  • 算法分析:

     时间复杂度为:O log(n);
     平均查找长度:ASL=log(n+1)-1;
    
1.3分块查找(了解)
  • 分块查找法要求将列表组织成以下索引顺序结构:
    (1)首先将列表分成若干个块(子表)。一般情况下,块的长度均匀,最后一块可以不满。每块中元素任意排列,即块内无序,但块与块之间有序。
    (2)构造一个索引表。其中每个索引项对应一个块并记录每块的起始位置,和每块中的最大关键字(或最小关键字)。索引表按关键字有序排列。

  • 分块查找的基本过程为:

    (1)首先,将待查关键字K与索引表中的关键字进行比较,以确定待查记录所在的块。具体的可用顺序查找法或折半查找法进行。
    (2)进一步用顺序查找法,在相应块内查找关键字为K的元素。
    在这里插入图片描述

2.基于树的查找方法

基于树的查找法(树表查找法),是将待查表组织成特定树的形式并在树结构上实现查找的方法 。

2.1二叉排序树
  • 二叉排序树(二叉查找树):二叉树排序树或者是一棵空树,或者是具有如下性质的二叉树:

     (1)若左子树非空,则左子树上所有结点的值均小于根结点的值;
     (2)若右子树非空,则右子树上所有结点的值均大于根结点的值;
     (3)左右子树也分别为二叉排序树。
    
  • 二叉排序树性质:中序遍历一个二叉排序树时可以得到一个递增有序序列。

  • 根据二叉排序树的定义我们可以画出一个二叉树例子:

    二叉搜索树

  • 二叉排序树的操作:

  • 二叉排序树的插入和生成:已知一个关键字值为 key 的结点 s,若将其插入到二叉排序树中,只要
    保证插入后仍符合二叉排序树的定义即可。
    (1)算法思想:

       	1、若二叉排序树是空树,则 key 成为二叉排序树的根。
     	2、若二叉排序树非空,则将 key与二叉排序树的根进行比较:
     			a.如果 key 的值等于根结点的值,则停止插人。
     			b.如果 key 的值小于根结点的值,则将 key 插人左子树。
     			c.如果 key 的值大于根结点的值,则将 key 插入右字树。
    

(2)过程:
在这里插入图片描述

  • 二叉排序树的查找:因为二叉排序树可看作是一个有序表,所以在二叉排序树上进行查找,和折半查找类似,也是一个逐步缩小查找范围的过程。
    (1)算法思想:首先将待查关键字 key 与根结点关键字t进行比较,如果:

     	① key=t,则返回根结点地址;
     	② key<t,则进一步查左子树;
     	③ key>t,则进一步查右子树。
    
  • 二叉排序树的删除(难点):从二叉排序树中删除一个结点,不能把以该结点为根的子树都删去,只能删掉该结点,并且还要应保证删除后所得的二叉树仍然满足二叉排序树的性质不变。也就是说,在二叉排序树中删去一个结点相当于删去有序序列中的一个结点。
    (1)算法思想:

     ① 若 p 为叶结点,则可直接将其删除。
     ② 若 p 结点只有左子树,或只有右子树,则可将 p 的左子树或右子树,直接改为其双亲结点 f 的左子树。
     ③ 若 p 既有左子树,又有右子树。此时有以下两种处理方法:
     	a.方法 1:首先找到 p 结点在中序序列中的直接前驱 s。然后将 p的左子树改为工的左子树,
     	而将 p 的右子树改为s的右子树。
     	b.方法 2:首先找到 p 结点在中序序列中的直接前驱 s.然后用 s 结点的值替代 p 结点的值,
     	再将 s 结点删除,原s结点的左子树改为s的双亲结点q的右子树。
    
  • 讲了这么多想法,现在让我们用代码来解释

public class BinarySearchTree {

    public static class Node{
        int key;
        Node left;
        Node right;

        public Node(int key) {
            this.key = key;
        }
    }

    private Node root = null;

    //在搜索树中查找 key,如果找到,返回 key 所在的结点,否则返回 null
    public Node search(int key) {
        Node cur = root;
        while (cur != null) {
            if (key == cur.key) {
                return cur;
            } else if (key < cur.key) {
                cur = cur.left;
            } else {
                cur = cur.right;
            }
        }
            return null;
    }

    //插入表示插入成功, false 表示插入失败
    public boolean insert(int key) {
        if (root == null) {
            root = new Node(key);
            return true;
        }
        Node cur = root;
        Node parent = null;
        while (cur != null) {
            if (key == cur.key) {
                return false;
            } else if (key < cur.key) {
                parent = cur;
                cur = cur.left;
            } else {
                parent = cur;
                cur = cur.right;
            }
        }
        Node node = new Node(key);
        if (key < parent.key) {
            parent.left = node;
        } else {
            parent.right = node;
        }
        return true;
    }
    /**
      * 删除成功返回 true,失败返回 false
      */
    public boolean remove(int key) {
        Node cur = root;
        Node parent = null;//记录 p 的双亲结点
        while (cur != null) {
            if (key == cur.key) {
                break;
            } else if (key < cur.key) {
                parent = cur;
                cur = cur.left;
            } else {
                parent = cur;
                cur = cur.right;
            }
        }

        // 该元素不在二叉搜索树中
        if(null == cur){
            return false;
        }
        //找到了要删除的结点 cur,其双亲结点为 parent
        if (cur.left == null){                  // cur 没有左子树。
            if (parent == null){                // 表示 cur 为根结点,接下来只需要让 root 指向 cur 的右子树(无论为不为 null,结果一样)
                root = cur.right;
            }else if (parent.left == cur){      // 表示 cur 作为 parent 左子树的身份,所以需要让 parent 的左孩子指向 cur 的右子树。
                parent.left = cur.right;
            }else {                             // 表示 cur 作为 parent 右子树的身份,所以需要让 parent 的左孩子指向 cur 的右子树。
                parent.right = cur.right;
            }
        }else {                                 // cur 有左子树。
            Node q = cur;                       // 
            Node s = cur.left;                  // 寻找 cur 左子树中的最右下结点:可以思考一下,结合上面讲的算法思想,为什么是?
            while (s.right != null){
                q = s;
                s = s.right;
            }
            if (q == cur){                      // 表示 左子树中最右下的结点就是 cur 的左孩子,
                q.left = s.left;
            }else {                             // 将最右下结点 s 的左子树链接到其双亲节点 q 的右孩子。
                q.right = s.left;
            }
            cur.key = s.key;
        }
        
        return true;
    }
}
2.2 平衡二叉排序树

平衡二叉排序树又称为AVL树。一棵平衡二叉排序树或者是空树,或者是具有下列性质的二叉排序树:

(1)左子树与右子树高度之差的绝对值小于等于1;

(2)左子树和右子树也是平衡二叉排序树。

3.计算式查找----哈希法

3.1构造原则:
①函数本身便于计算 ;
②计算出来的地址分布均匀,即对任一关键字k,H(k) 对应不同地址的概率相等,目的是尽可能减少冲突。
3.2构造方法:
①数字分析法:如果事先知道关键字集合,并且每个关键字的位数比哈希表的地址码位数多时,
 可以从关键字中选出分布较均匀的若干位,构成哈希地址。
②平方取中法:当无法确定关键字中哪几位分布较均匀时,可以先求出关键字的平方值,然后
 按需要取平方值的中间几位作为哈希地址。
3.3 处理冲突的方法(四种)
1.开放定址法 (再散列法 )
	hi = ( H(key)+di ) % m i=1,2,···,n
	(1)线性探测再散列:di = 1,2,3,···,m-1
		即当发生冲突时顺序查看表中的下一个单元,直到找出一个空单元。
	(2)二次探测再散列:di =1^2,-1^2,2^2,-2^2,···,k^2,-k^2 (k<=m/2)
		即当发生冲突时,在表的左右进行跳跃探测,比较灵活。
	(3)伪随机探测再散列:di=伪随机数序列
		具体实现应该建立一个伪随机序列。
2.再哈希法
     同时构造多个哈希函数:Hi = RHi(key) i=1,2,···,k
3.链地址法:
	    这种方法的基本思想是将所有哈希地址为 i的元素构成一个称为同义词链的单链表,
	并将单链表的头指针存在哈希表的第i个单元中,因而查找,插人和删除主要在同义词
	链中进行。链地址法适用于经常进行插人和删除的情况。
4.建立公共溢出区:
	将哈希表分为基本表和溢出表两部分,凡是和基本表发生冲突的元素一律填入溢出表,

在这里插入图片描述

3.4 哈希表的查找过程

当查找关键字K时,首先计算p0= hash(K)。如果单元p0为空,则所查元素不存在;如果单元p0中元素的关键字为K,则找到所查元素;否则重复下述解决冲突的过程:按解决冲突的方法,找出下一个哈希地址pi ,如果单元pi为空,则所查元素不存在;如果单元pii中元素的关键字为K,则找到所查元素。

3.5负载因子
散列表的载荷因子定义为: a =填入表中的元素个数/散列表的长度
a是散列表装满程度的标志因子。由于表长是定值,a与“填入表中的元素个数”成正比,所以,a越大,表明填入表
中的元素越多,产生冲突的可能性就越大:反之,a越小,标明填入表中的元素越少,产生冲突的可能性就越小。
实际上,散列.表的平均查找长度是载荷因子a的函数,只是不同处理冲突的方法有不同的函数。对于开放定址法,
荷载因子是特别重要因素,应严格限制在0. 7-0. 8以下。超过0. 8,查表时的CPU缓存不命中(cachemissing)按照指
数曲线上升。因此,一些采用开放定址法的hash库,如Java的 系统库限制了荷载因子为0.75,超过此值将resize散
列表。
3.6代码实现
package Exercises;


public class HashBucket {
    private static class Node {
        private int key;
        private int value;
        Node next;
        public Node(int key, int value) {
            this.key=key;
            this.value=value;
        }
    }
    private Node[] array;
    private int size; // 当前的数据个数
    private static final double LOAD_FACTOR=0.75;

    public int put(int key, int value) {
        int index = key % array.length;
        // 在链表中查找 key 所在的结点
        // 如果找到了,更新
        // 所有结点都不是 key,插入一个新的结点
        for (Node cur = array[index]; cur != null; cur = cur.next) {
            if (key == cur.key) {
                int oldValue = cur.value;
                cur.value = value;
                return oldValue;
            }
        }
        Node node = new Node(key, value);
        node.next = array[index];
        array[index] = node;
        size++;
        if (loadFactor() >= LOAD_FACTOR) {
            resize();
        }
        
        return  -1;
    }
    private void resize(){
        Node[] newArray=new Node[array.length*2];
        for (int i=0; i<array.length; i++) {
            Node next;
            for (Node cur=array[i]; cur!=null; cur=next) {
                next=cur.next;
                int index = cur.key%newArray.length;
                cur.next = newArray[index];
                newArray[index] =cur;
            }
        }
        array=newArray;
    }

    private double loadFactor(){
        return size*1.0/array.length;
    }

    public void HashBucket(){
        array=new Node[8];size=0;
    }

    public int get(int key) {
        int index = key%array.length;
        Node head=array[index];
        for (Node cur=head; cur!=null; cur=cur.next) {
            if (key==cur.key) {
                return cur.value;
            }
        }
        return-1;
    }
}

4.总结—与 java 有关的类

(1) HashSet :无序集合,元素不重复 
(2) TreeSet:有序集合,元素不重复。
(3) HashMap:无序键值对
(4) TreeMap:有序键值对,其中以 key 为排序标准。

其中他们的继承关系在之前的文章中提到过:数据结构基础。
关于Set和Map将在下一章节介绍。

  • 8
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值