《算法》学习笔记(3)—— 查找

一:基本概念

査找表:是由同一类型的数据元素(或记录)构成的集合。

  1. 静态査找表:只作査找操作的査找表。 査询某个特定的数据元素是否在査找表中, 检索某个特定的数据元素和各种属性。
  2. 动态査找表:在査找过程中同时插入査找表中不存在的数据元素,或者从査找表中删除巳经存在的某个数据元素。

关键字:是数据元素中某个数据项的值,又称为键值,用它可以标识一个数据元素。也可以标识一个记录的某个数据项(字段),我们称为关键码。

  • 主关键字:可以唯一地标识一个记录的关键字,它所在的数据项称为主关键码。
  • 次关键字:可以识别多个数据元素 / 记录的关键字,它对应的数据项就是次关键码。

查找,就是根据给定的某个值,在査找表中碥定一个其关键字等于给定值的数据元素(或记录)。

  • 若表中存在这样的一个记录,则称査找是成功的,此时查找的结果给出整个记录的信息,或指示该记录在査找表中的位罝。
  • 若表中不存在关键字等于给定值的记录,则称査找不成功,此时查找的结果可给出一个空记录或空指针。

二:顺序表查找(线性查找)

基本思想

  1. 它从表中第一个(或最后一个)记录开始,逐个进行记录的关键字和给定值比较。
  2. 若某个记录的关键字和给定值相等,则査找成功,找到所査的记录;
  3. 如果直到最后一个(或第一个)记录,关键字和给定值比较都不等时,则査找不成功。

流程

数组:

  • 遍历数组,在数组a (注意元素值从下标1开始)中査看有没有关键字。(但是每次都要判断是否越界)
  • 遍历数组,先将a[0]设置为关键值,再从后往前遍历数组查看有没有关键字。(无需判断是否越界)

链表:

  • 查找:顺序查找链表并返回关键字
  • 更改:顺序查找关键字,找到则更改值,没有找到则插入新的值

时间复杂度

查找:

  • 未命中的查找需要比较N次(因为表中不能有重复元素)
  • 随机命中的查找需要比较N/2次

插入:

  • 插入时需要比较N次
  • 向一个空表插入不同的键时需要比较N2/2次

三:有序表查找

1. 二分查找法(折半査找技术)——适合静态查找

条件

线性表中的记录必须是关键码有序(通常从小到大有序),线性表必须采用顺序存储。

基本思想
  1. 在有序表中,取中间记录作为比较对象。
  2. 若给定值与中间记录的关键字相等,则査找成功;
  3. 若给定值小于中间记录的关键字,则在中间记录的左半区继续査找;
  4. 若给定值大于中间记录的关键字,则在中间记录的右半区继续査找。
  5. 不断重复上述过程,直到査找成功,或所有査找区域无记录,査找失败为止
流程
  1. 创建标记A记录最低下标1,标记B记录最高下标n
  2. 当A小于B时开始查找,取mid为A和B的一半,如果关键字比mid小,则把B调整为中间坐标mid的小一位,否则把A调整为中间坐标mid的大一位。
  3. 若mid和关键字相等则找到,否则没有找到。
public class BinarySearch {
    public static int binart_search(int[] arr,int key){
        int index = -1,low = 0,high = arr.length-1,mid = 0;
        while (low<=high)
        {
            mid = (low+high)/2;         //中间值
            if(arr[mid]>key)            //比中间的小,修改high
                high = mid-1;
            else if(arr[mid]<key)       //比中间的大,修改low
                low = mid+1;
            if (arr[mid]==key)          //命中
            {
                index = mid;
                break;
            }
        }
        return index;
    }
    public static void main(String[] args) {
        int[] a = {1,2,3,4,5,6,7,8,9,10};
        int key = 11;
        int index = binart_search(a,key);
        System.out.println(index);
    }
 }
特点

适合关键字极端,表长较小。

时间复杂度
  • 查找:最多需要logN+1次
  • 插入:最多需要访问N2次数组

2. 差值查找法(改进二分查找)

区别

根据要査找的关键字key与査找表中最大最小记录的关键字比较后的査找方法,
原来:mid = low+ (high-low) * 1/2
改进:mid = low+ (high-low) * ( key-a[low] ) / ( a [high]-a [low]);

特点

适合关键字分布均匀,表长较大。

3. 斐波那契查找法(黄金分割)

特点

采用最接近查找长度和查找个数的斐波那契数值来确定拆分点。

流程
  1. 创建标记A记录最低下标1,标记B记录最高下标n,使用斐波那契数列F
  2. 计算要查找的数组X最大下标n位于F的位置 i
  3. 以F[i]-1为分隔点,如果数组X的最大下标n比F[i]-1小,那么就把数组X的n到i的空位用它的最后一个元素补全(避免后面越界比较失败)
  4. 当A小于B时开始查找,取mid=A+F[i-1]-1,如果关键字比mid小,则把B调整为中间坐标mid的小一位,同时i退一位,此时的范围为(low,mid-1),个数为F[I-1]-1个。
  5. 否则把A调整为中间坐标mid的大一位,同时i退两位,此时的范围为(mid+1,high),个数为F[i-2]-1个。
  6. 若mid和关键字相等则找到,否则是补全数值。

四:线性索引查找

定义:索引是为了加快査找速度而设计的一种数据结构。索引就是把一个关键字与它对应的记录相关联的过程。

组成:一个索引由若干个索引项构成,每个索引项至少应包含关键字和其对应的记录在存储器中的位置等信息。

索引按照结构可以分为线性索引、树形索引和多级索引。所谓线性索引就是将索引项集合组织为线性结构,也称为索引表。

1. 稠密索引

建立索引表:

用一张索引表,将数据集中的每个记录对应一个索引项,索引项按照关键码有序排列。

流程:
  1. 在索引表中查找要查关键字所在的索引项。由于索引表的索引项有序的,因此很容易利用折半、插值等算法得到结果。
  2. 根据索引直接获取结果
特点:

使用空间换时间,索引项和数据集的记录个数相同,适合比较小的数据集。

2. 分块索引

建立索引表

对数据集进行分块,使其分块有序。

  • 块内无序:即每一块内的记录不要求有序。
  • 块间有序:要求第 i 块所有记录的关键字均要大于第i -1 块中所有记录的关键字。

然后再用一张索引表对每一块建立一个索引项,从而减少索引项的个数。

  • 最大关键码,它存储每一块中的最大关键字,便于下一块的比较
  • 存储了块中的记录个数,便于循环时
  • 用于指向块首数据元素的指针,便于对这一块中记录进行遍历
流程
  1. 在分块索引表中查找要查关键字所在的块。由于分块索引表是块间有序的,
    因此很容易利用折半、插值等算法得到结果。
  2. 根据块首指针找到相应的块,并在块中顺序査找关键码。因为块中可以是无序
    的,因此只能顺序查找。

3. 倒排索引

根据属性(或字段、次关键码)的值来査找记录。

建立索引表
  • 次关键码:需要查找的关键字
  • 记录号表:存储具有相同次关键字的所有记录的记录号(可以是指向记录的指针或者是该记录的主关键字)。
特点

查找速度快,但是记录号不定长

五:二叉排序树查找(向下生长)

建立树
  • 若它的左子树不空,则左子树上所有结点的值均小于它的根结构的值;
  • 若它的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
  • 它的左、右子树也分别为二叉排序树。
流程

查找:

  1. 判断二叉树是否到达叶子结点,如果是则证明查找失败
  2. 如果关键字与结点匹配,则查找成功
  3. 如果关键字比结点小,则向左孩子递归查找
  4. 如果关键字比结点大,则向右孩子递归查找
  //获取值
    public V get(K key) {
        return get(root, key);
    }

    private V get(TreeNode<K, V> node, K key) {
        if (node == null)
            return null;
        if (key.compareTo(node.getKey()) > 0)
            return get(node.getRight(), key);
        else if (key.compareTo(node.getKey()) < 0)
            return get(node.getLeft(), key);
        else
            return node.getValue();
    }

插入:

  1. 如果查找失败,则创建一个新结点并赋值,令左右孩子为空
  2. 如果二叉树到达叶子结点,则直接插入新结点
  3. 如果关键字比结点小,则向左孩子递归插入
  4. 如果关键字比结点大,则向右孩子递归插入
  //插入或更新值
    public void put(K key, V value) {
        put(root, key, value);
    }

    private TreeNode<K, V> put(TreeNode<K, V> node, K key, V value) {
        if (node == null)
            return new TreeNode<>(key, value,1);
        else if (key.compareTo(node.getKey()) > 0)
            node.setRight( put(node.getRight(), key, value));
        else if (key.compareTo(node.getKey()) < 0)
            node.setLeft( put(node.getLeft(), key, value));
        else
            node.setValue(value);
        node.setSize(size(node.getLeft())+size(node.getRight())+1); //维护结点数量
        return node;
    }

删除

  • 如果删除叶子节点,则没有影响
  • 如果删除只有左子树 / 右子树的结点,则将整个子树移动到被删除的结点的位置
  • 如果删除的结点同时有左右孩子,则找到被删除的结点的直接前驱(或直接后继)代替。
  //获取最小值,也就是最左边的树
    private TreeNode<K,V> min(TreeNode<K,V> node){
        if (node == null)
            return null;
        if (node.getLeft() == null)
            return node;
        return min(node.getLeft());
    }
  //删除最小值
    private TreeNode<K,V> delMin(TreeNode<K,V> node){
        if(node == null)
            return null;
        if(node.getLeft() == null) //如果没有左子树,则当前结点为最小值,删除当前结点,用比顶点大的值代替
            return node.getRight();
        node.setLeft(delMin(node.getLeft())); //如果有左子树,则需要删除最左边的结点,用左子树的第二个大的结点代替
        node.setSize(size(node.getLeft())+size(node.getLeft())+1);  //重新更新结点个数
        return node;
    }

//删除结点
    public void delete(K key){
        delete(root,key);
    }

    private TreeNode<K,V> delete(TreeNode<K,V> node,K key){
        if(node == null)
            return null;
        if (node.getKey().compareTo(key) > 0)
            node.setLeft(delete(node.getLeft(),key));
        else if (node.getKey().compareTo(key) < 0)
            node.setRight(delete(node.getRight(),key));
        else {
            //用一个孩子顶替
            if (node.getLeft() == null)
                return node.getRight();
            else if (node.getRight() == null)
                return node.getLeft();
            //找出孩子中较大的顶替,也就是右孩子中的最左结点,把原来的左子树作为左子树,右孩子的剩余结点作为右子树
            else
            {
                TreeNode<K,V> temp = node;
                node = min(temp.getRight());
                node.setRight(delMin(temp.getRight()));
                node.setLeft(temp.getLeft());
            }
        }
        node.setSize(node.getLeft().getSize()+node.getRight().getSize()+1);
        return node;
    }
特点
  • 二叉排序树是以链接的方式存储,插入删除的时间性能比较好。
  • 二叉排序树的査找,走的就是从根结点到要査找的结点的路径,其比较次数等于给定值的结点在二叉排序树的层数,取决于二叉排序树的形状。可以构建平衡二叉树。
  • 在最坏情况下,对于N个数,插入和查找需要比较N次,在平均下需要1.39lgN次

平衡树

  • AVL树:左右子树的高度差不大于1
  • 红黑树:左右子树的链的高度差不超过两倍
  • SB树:一个左子树的结点个数不小于它父亲的右子树的任一个子树

六:多路查找树

2-3树

(1)定义:

2-3树的每一个结点都具有两个孩子(2结点)或三个孩子(3结点)。并且2-3树中所有的叶子都在同一层次上。

  • 2结点包含一个元素和两个孩子(或没有孩子,不能只有一个孩子),左子树包含的元素小于该元素,右子树包含的元素大于该元素。
  • 一个3结点包含一小一大两个元素和三个孩子(或没有孩子),左子树包含小于较小元素的元素,右子树包含大于较大元素的元素,中间子树包含介于两元素之间的元素。
(2)高效的插入并维持平衡:
  • 空树:直接插入包含一个元素的2结点即可。
  • 2结点:由于2结点只有一个元素,可以将其升级为3结点,拥有两个元素。插入的元素与当前叶子结点的元素比较大小后,决定左右位置。
  • 3结点。因为3结点本身已经有两个元素了,需要将其拆分和移动。
  1. 如果3结点A没有双亲结点,则关键字与A结合为4结点,在三者中选择中间值向上移动一层,作为双亲节点,剩下两个分别为子结点。
  2. 如果3结点A的双亲结点B是一个2结点,则关键字与A结合为4结点,在三者中选择中间值向上移动一层,与双亲结点B结合为3结点,剩下两个分别为子结点(2结点)。
  3. 如果3结点A的双亲结点B是一个3结点,B的双亲结点C是一个2结点,则关键字与A结合为4结点,在三者中选择中间值向上移动一层,与双亲结点B结合为4结点,剩下两个分别为子结点(2结点)。然后在B结点中选择中间值向上移动一层,与结点C结合为3结点,剩下两个分别为子节点(2结点)。
  4. 如果3结点A的双亲结点B也是一个3结点,B的双亲结点C也是一个3结点,或者C是根节点,则关键字与A结合为4结点,在三者中选择中间值向上移动一层,与双亲结点B结合为4结点,剩下两个分别为子结点(2结点)。然后在B结点中选择中间值向上移动一层,与结点C结合为4结点,剩下两个分别为子节点(2结点)。结点C继续选择中间值向上移动一层,剩下两个分别为子节点(2结点)。
(3)特点:

在大小为N的2-3树中,查找和插入的次数小于等于lgN次

红黑树——JAVA中TreeMap的实现

(1)定义:用二叉树实现2-3树,也就是平衡二叉树

左斜的红色的链接连接2结点A,黑色的链接指向普通的2结点B。AB就是一个3结点

(2)流程:

在构建二叉排序树的过程中,每当插入一个结点时,先检査是否因插入而破坏了树的平衡性。若是,则找出最小不平衡子树。
在保持二叉排序树特性的前提下,调整最小不平衡子树中各结点之间的链接关系,进行相应的旋转,使之成为新的平衡子树。

1. 插入的平衡:插入3结点会产生连续两条红链接,插入2结点可能会产生一条右斜红链接
  1. 右红链接:左旋转(也就是把父结点升级为3结点)
  2. 左右子红连接:变色,(相当于把其送入父节点,对结点进行拆分)
  3. 上面左红链接下面左红链接::右旋转,转化为 2(也就是与父节点结合后上移)
  4. 上面左红链接下面右红链接:左旋转,转化为3(也就是把父节点升级后结合)

(1)旋转:主要是把中间结点从一个结点的孩子转为另一个结点的孩子

  • 左旋转:把原来小节点作为根节点,变成大节点(带上自己的右孩子)作为根节点。
  1. 创建新的树R,指向原来的树P的右子树
  2. 把R的左子树送给P,成为P的右子树(R的左孩子代替R)
  3. 将R的左子树指向P(P代替R的左孩子)
  4. 最后将P指向R
 //左旋转
    private RedBlackNode<K,V> rotateLeft(RedBlackNode<K,V> node){
        //转换根节点
        RedBlackNode<K,V> temp = node.getRight();
        node.setRight(temp.getLeft());
        temp.setLeft(node);
        //维护链接颜色
        temp.setColor(node.isColor());
        node.setColor(RedBlackNode.RED);
        //维护结点
        temp.setSize(node.getSize());
        node.setSize(size(node.getLeft())+size(node.getRight())+1);
        return temp;
    }

  • 右旋转:把原来大节点作为根节点,变成小节点(带上自己的左节点)作为根节点
  1. 创建新的树L,指向原来的树P的左子树
  2. 把L的右子树送给P,成为P的左子树(L的右孩子代替L)
  3. 将L的右子树指向P(P代替L的右孩子)
  4. 最后将P指向L
    //右旋转
    private RedBlackNode<K,V> rotateRight(RedBlackNode<K,V> node){
        //转换根节点
        RedBlackNode<K,V> temp = node.getLeft();
        node.setLeft(temp.getRight());
        temp.setRight(node);
        //维护链接颜色
        temp.setColor(node.isColor());
        node.setColor(RedBlackNode.RED);
        //维护结点
        temp.setSize(node.getSize());
        node.setSize(size(node.getLeft())+size(node.getRight())+1);
        return temp;
    }

(2)颜色转化:当一个结点同时具有两个红色链接并且该结点是中间值,可以直接转化为黑色链接

//转换颜色,把两个孩子变为黑色链接,同时自己变为红色链接
    private void changeColor(RedBlackNode<K,V> node){
        node.setColor(RedBlackNode.RED);
        RedBlackNode<K,V> temp = node.getLeft();
        temp.setColor(RedBlackNode.BLACK);
        temp = node.getRight();
        temp.setColor(RedBlackNode.BLACK);
    }

(3):插入

  1. 根节点默认为黑色链接 ,插入的结点默认为红色链接
  2. 先插入,后平衡
//插入,初始化根节点颜色
    public void put(K key,V value){
        root = put(key,value,root);
        root.setColor(RedBlackNode.BLACK);
    }

    private RedBlackNode<K,V> put(K key,V value,RedBlackNode<K,V> node){
        //创建新结点
        if(node == null)
            return new RedBlackNode<>(key,value,1,RedBlackNode.RED);
        //查找结点
        if(key.compareTo(node.getKey()) < 0)
            node.setLeft(put(key,value,node.getLeft()));
        else if(key.compareTo(node.getKey()) > 0)
            node.setRight(put(key,value,node.getRight()));
        else
            node.setValue(value);
          //平衡树
        if(isRed(node.getRight()) && !isRed(node.getLeft()))
            node = rotateLeft(node);
        if (isRed(node.getLeft()) && isRed(node.getLeft().getLeft()))
            node = rotateRight(node);
        if(isRed(node.getRight()) && isRed(node.getLeft()))
            changeColor(node);
        node.setSize(size(node.getLeft()) + size(node.getRight()) + 1);
        return node;
    }
2:删除的平衡:因为红黑树不可能有空链接,所以删除时需要维持平衡
  1. 删除3结点可以直接删除
  2. 删除2结点时将2结点转换为3结点或4结点,再直接删除。
  • 如果被删除结点A的兄弟结点B是3结点,则A向B借键组成3结点(根节点向B借键,A向根节点借键)
  • 如果被删除结点A的兄弟结点B也是2结点,则把根结点和两个结点AB结合为4结点(根节点的父节点降级)
  1. 用后继结点代替,然后再平衡树

(1)转换:主要是把子节点加上红色链接升级,父节点加上黑色链接降级

  • 左转移:把左孩子升级为3结点或4结点
 //升级结点为3结点
    private RedBlackNode<K,V> moveRedLeft(RedBlackNode<K,V> node){
        if(node == null)
            return null;
        if(!isRed(node) || isRed(node.getLeft()) || isRed(node.getLeft().getLeft()))
            return null;
        //父节点与孩子结合成为4节点
        changeColor(node);
        //如果父节点的右孩子是3节点,则可以借键给左孩子
        if (node.getRight() != null && isRed(node.getRight().getLeft()))
        {
            //父节点向右孩子借键,也就是把右孩子中比较小的结点移入父节点
            node.setRight(rotateRight(node.getRight()));//右旋转,使得小节点作为父节点。再移入父节点
            //左孩子向父节点借键,也就是把父节点移入左孩子
            node = rotateLeft(node);
        }
        return node;
    }


  • 右转移:把右孩子升级为3结点或4结点
  //升级结点为3结点
    private RedBlackNode<K,V> moveRedRight(RedBlackNode<K,V> node){
        if(node == null)
            return null;
        if(!isRed(node) || isRed(node.getRight()) || isRed(node.getRight().getLeft()))
            return null;
        //父节点与孩子结合成为4节点
        changeColor(node);
        //如果父节点的左孩子是3结点,可以借键给右孩子
        //右孩子向父节点借键,也就是把父节点移入右孩子
        if (isRed(node.getLeft().getLeft()))
            node = rotateRight(node);   //不用旋转,因为右边肯定是大节点
        return node;
    }

(2)平衡:删除结点之后调整树


    //平衡树
    private RedBlackNode<K,V> balance(RedBlackNode node) {
        //右斜调整
        if (isRed(node.getRight()))
            node = rotateLeft(node);
        //连续两条左斜链接
        if (isRed(node.getLeft()) && isRed(node.getLeft().getLeft()))
            node = rotateRight(node);
        //4结点调整
        if (isRed(node.getLeft()) && isRed(node.getRight()))
            changeColor(node);
        node.setSize(size(node.getLeft()) + size(node.getRight()) + 1);
        return node;
    }

(3):删除

  • 删除最小值:
  1. 由上向下先升级结点为3结点4结点
  2. 不断找到左子树
  3. 删除后由下向上调整树
 //删除最小值
    public void delMin(){
        if(isEmpty())
            return;
        //先将根节点升级为3结点
        if (!isRed(root.getLeft()) && !isRed(root.getRight()))
            root.setColor(RedBlackNode.RED);
        root = delMin(root);
        //再初始化根节点的链接
        if (!isEmpty()) root.setColor(RedBlackNode.BLACK);
    }

    private RedBlackNode<K,V> delMin(RedBlackNode<K,V> node){
        if(node.getLeft() == null)  //如果没有左子树,则当前结点为最小值,直接删除
            return null;
        //当前结点不是3结点,结点的左孩子也不是3结点,先升级
        if (!isRed(node.getLeft()) && !isRed(node.getLeft().getLeft()))
            node = moveRedLeft(node);
        //递归删除
        node.setLeft(delMin(node.getLeft()));
        //重新调整树
        return balance(node);
    }
  • 删除最大值:
  1. 因为不能直接删除3结点的右边结点,所以先把所有红链接右斜
  2. 由上向下先升级结点为3结点或4结点
  3. 不断找到右子树
  4. 删除后由下向上调整树
 //删除最大值
    public void delMax(){
        if(isEmpty())
            return;
        //先将根节点升级为3结点
        if (!isRed(root.getLeft()) && !isRed(root.getRight()))
            root.setColor(RedBlackNode.RED);
        root = delMax(root);
        //再初始化根节点的链接
        if (!isEmpty()) root.setColor(RedBlackNode.BLACK);
    }

    private RedBlackNode<K,V> delMax(RedBlackNode<K,V> node){
        //如果左孩子结点为红结点,先将所有的红链接右斜,转换为右孩子结点为红结点
        if(isRed(node.getLeft()))
            node = rotateRight(node);
        if(node.getRight() == null)  //如果没有右子树,则当前结点为最小值,直接删除
            return null;
        //再把右孩子的红链接左斜
        if (!isRed(node.getRight()) && !isRed(node.getRight().getLeft()))
            node = moveRedRight(node);
        //递归删除
        node.setRight(delMax(node.getRight()));
        //重新调整树
        return balance(node);
    }

删除:

  • 如果在左子树,需要先由上至下升级结点
  • 如果在右子树,需要先由上至下右斜
  1. 如果是右子树的叶子节点,因为已经是3结点的右侧结点,直接删除
  2. 如果是右子树的非叶子结点,需要先由上至下升级,然后再代替
   //删除
    public void delete(K key){
         if (!contains(key))
            return;
        //先将根节点升级为3结点
        if (!isRed(root.getLeft()) && !isRed(root.getRight()))
            root.setColor(RedBlackNode.RED);
        root = delete(root,key);
        if(!isEmpty())
            root.setColor(RedBlackNode.BLACK);
    }

    private RedBlackNode<K,V> delete(RedBlackNode<K,V> node,K key){
        //关键字在左子树,先把左结点由上至下升级
        if (node.getKey().compareTo(key) >= 0){
            if (!isRed(node.getLeft()) && !isRed(node.getLeft().getLeft()))
                node = moveRedLeft(node);
            node.setLeft(delete(node.getLeft(),key));
        }
        //关键字在右子树,先把右结点由上至下升级
        else
        {
            //升级前先右斜
            if (isRed(node.getLeft()))
                node = rotateRight(node);
            //找到了,并且在叶子结点可以直接删除,在向下查找的过程中,已经保证了结点不可能是2-结点
            if (node.getKey().compareTo(key) == 0 && (node.getRight() == null))
                return null;
            //把右结点由上至下升级
            if (!isRed(node.getRight()) && !isRed(node.getRight().getLeft()))
                node = moveRedRight(node);
            //找到了,并且不在叶子节点,用后面的结点代替
            if (node.getKey().compareTo(key) == 0) {
                RedBlackNode<K,V> right = min(node.getRight());
                node.setKey(right.getKey());
                node.setValue(right.getValue());
                node.setRight(delMax(node.getRight()));
            }
            //没找到,继续在右子树找
            else
                node.setRight(delete(node.getRight(),key));
        }
        //平衡树
        return balance(node);
    }

八:散列表查找

1. 基本概念

在记录的存储位置和它的关键字之间建立一个确定的对应关系f,使得每个关键字key对应一个存储位置f (key)。查找时,根据这个确定的对应关系找到给定值key的映射 f(key), 若査找集合中存在这个记录,则必定在f(key)的位置上。

对应关系 f 称为散列函数,又称为哈希函数。
记录存储在一块连续的存储空间中,这块连续存储空间称为散列表或哈希表。
关键字对应的记录存储位置我们称为散列地址。

2. 缺点

  • 关键字对应很多记录的情况
  • 不适合范围査找

3. 关键因素

  • 散列函数是否均匀
  • 处理冲突的方法
  • 散列表的装填因子(a = 填入表中的记录个数/散列表长度),散列表的平均査找长度取决于装填因子,而不是取决于査找集合中的记录个数。

4. 查找

  1. 在存储时,通过散列函数计算记录的散列地址,并按此散列地址存储该记录。
  2. 当査找记录时,我们通过同样的散列函数计算记录的散列地址,按此散列地址访问该记录。

平均查找长度L=散列的次数/散列的个数

(1) 构造散列函数
原则
  • 计算简单
  • 散列地址分布均匀,有较好的散列性
  • 函数应该是压缩映像函数
方法
1:直接定址法

取关键字的某个线性函数值为散列地址,f ( key ) =a * key+b ( a、b 为常数)

  • 优点是简单、均匀,不会产生冲突
  • 需要事先知道关键字的分布
  • 査找表较小且连续
2:数字分析法

抽取关键字的一部分来计算散列存储位置

  • 需要事先知道关键字的分布
  • 关键字的若干位分布较均匀
  • 关键字位数较大
3:平方取中法

对关键字进行平方,再取中间的几位

  • 不知道关键字的分布
  • 关键字位数不大
4:折叠法

将关键字从左到右分割成位数相等的几部分(注意最后一部分位数不够时可以短些),然后将这几部分叠加求和,并按散列表表长,取后几位作为散列地址。

  • 不知道关键字的分布
  • 关键字位数很大
5:除留取余法

对关键字取模,f ( key) = key mod p ( p<=m )
若散列表表长为m,通常p为小于或等于表长(最好接近m)的最小质数或不包含小于20质因子的合数。


    public int hash(int prime,K target){
        return (target.hashCode() & 0x7fffffff) % prime;
    }

    public int hash(K target){
        return (target.hashCode() & 0x7fffffff) % 31;
    }

  • 整数
  //整数
    public static int hashInteger(int prime,int num){
        return num % prime;
    }
  • 字符串
   //字符串
    public static int hashString(int R,int prime,String  target){
        int hash = 0;
        for (int i = 0; i < target.length(); i++) {
            hash = (R * hash + target.charAt(i)) % prime;
        }
        return hash;
    }
  • 浮点数
 //浮点数
    public static int hashDouble(int prime,double target) throws Exception {
        String binary = AllToBinary(target);
        return hashString(31,31,binary);
    }
 //二进制转换
    public static String IntegerToBinary(int target){
        StringBuffer buffer = new StringBuffer();
        while (target != 0)
        {
            buffer.append(target % 2);
            target = target / 2;
        }
        return buffer.reverse().toString();
    }
    public static String DoubleToBinary(double target)throws Exception {
        return DoubleToBinary(target,8);
    }
        public static String DoubleToBinary(double target,int count)throws Exception{
        StringBuffer buffer = new StringBuffer();
        if (count > 32 || count < 0)
            throw new Exception("Error Bits!");
        while (count >= 0)
        {
            target *= 2;
            if (target >= 1)
            {
                target -= 1;
                buffer.append(1);
            }
            else
                buffer.append(0);
            count--;
        }
        return buffer.toString();
    }

    public static String AllToBinary(double target)throws Exception{
        int inte = (int) target;
        double doub = target - inte;
        StringBuffer buffer = new StringBuffer();
        buffer.append(IntegerToBinary(inte));
        buffer.append(".");
        buffer.append(DoubleToBinary(doub));
        return buffer.toString();
    }
6:随机数法

选择一个随机数,取关键字的随机函数值为它的散列地址,f (key)=random (key)

  • 关键字的长度不等
(2)处理冲突
1:链地址法——JAVA中HashMap的实现

将所有关键字为同义词的记录存储在一个单链表中,我们称这种表为同义词子
表,在散列表中只存储所有同义词子表的头指针。

  • 需要遍历单链表
  • 在最坏情况下,含有M条链的N个键的散列表中,未命中和插入的次数约为N/M,也就是数组中链表的长度
 public V get(K key){
        if (isEmpty())
            return null;
        int index = hash(key);
        V value = (V) table[index].get(key);
        return value;
    }
2:开放地址法

一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。

  • 线性探测法(溢出处理需要另编程序,易产生聚集现象):fi ( key ) = (f ( key ) +di) MOD m ( di=1,2,3,… ,m-1 )
  • 双向探测法:fi ( key ) = (f ( key ) +di) MOD m(di=12,-12,22,-22,… ,q2,-q2,q<=m/2)
  • 随机探测法:在冲突时,对于位移量di采用随机函数计算得到

特点:

  • 耗费时间
  • 性能取决于N/M,即已使用的空间比例
  • 平均成本取决于多个键聚集后产生的箭簇,可以动态调整大小
    //查找键值对
    public V get(K key){
        if (isEmpty())
            return null;
        int index = hash(key);
        while (keys[index] != null) {
            if (keys[index].compareTo(key) == 0)    //命中
                return values[index];
            index = (index + 1) % keys.length;  //线性勘测法
        }
        return null;    //未命中
    }
3:再散列函数法

毎当发生散列地址冲突时,就换一个散列函数计算

4:公共溢出区法

在査找时,对给定值通过散列函败计算出散列地址后,先与基本表的相应位置进
行比对,如果相等,则査找成功;如果不相等,则到溢出表进行顺序査找。

5. 流程

  1. 定义散列函数F
  2. 建立散列表:通过散列函数计算关键字的散列地址,插入散列表。如果地址不为空则发生冲突,处理冲突后插入。
  3. 查找:根据关键字通过散列函数获取散列地址,如果地址上的关键字相同,则查找成功
  4. 如果不成功,则可能是发生冲突,处理冲突后再比对关键字。

九:比较

  • 无序链表的顺序查找(查找慢):查找一般需要N/2,插入需要N
  • 有序数组的二分查找(查找快):查找只需要lgN,插入一般需要N/2
  • 二叉树的查找(查找插入快,但是会不平衡):查找只需要lgN,插入只需要lgN
  • 红黑树(查找和插入稳定的快):查找只需要lgN,插入只需要lgN
  • 哈希表(需要一定的内存空间):查找和插入只需要常数时间
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值