《大话数据结构》第八章 查找


第八章 查找

定义

搜索引擎工作

查找表:是由同一类型的数据元素构成的集合。

关键字:是数据元素中某个数据项的值。

主关键字:若此关键字可以唯一地标识一个记录,则称此关键字为主关键字(Primary Key)。

次关键字:那些可以识别多个数据元素的关键字,称之为次关键词(Secondary Key)。

查找:根据给定的某个值,在查找表中确定一个关键字等于给定值的数据元素。

静态查找表:只作查找操作的查找表,主要操作有1. 查询某个“特定的”数据元素是否在表中;2. 检索某个“特定的”数据元素和各种属性。

动态查找表:在查找过程中同时插入查找表中不存在的数据元素,或者删除已经存在的元素。


顺序表查找

思路:从头到尾遍历比较。

实现代码

int Sequential_Search(int *a, int n, int key)
{
    int i;
    for (i=1; i<=n; i++)
    {
        if (a[i] == key)
            return i;
    }
    return 0;
}

优化顺序查找
设置一个哨兵,可以不需要 i 每次都和 n 比较。
做一个长度为n+1的数组,把0位置的值设为key,倒着查找每次下标减一,一定会出现值为key的时候,若下标为0表示没找到,否则表示找到。

int Sequential_Search2(int *a, int n, int key)
{
    int i;
    a[0] = key;
    i = n;
    while (a[i] != key)
    {
        i--;
    }
    return i;
}

时间复杂度
最好的情况为 O [ 1 ] O[1] O[1]
最坏的情况为 O [ n ] O[n] O[n]
关键字在任一位置的概率是相同的,所以平均查找次数为 n + 1 2 \frac{n+1}{2} 2n+1,最终时间复杂度为 O [ n ] O[n] O[n]


JAVA实现顺序查找

public class OrderFine {
    public static void main(String[] args) {
        int[] a = {2,3,4,5,1,6};
        int b = 4;
        Find1(a, b);
        // 第一个位置为空,写为0
        int[] c = {0, 2,3,4,5,1,6};
        Find2(c, b);
    }

    private static void Find2(int[] a, int b) {
        a[0] = b;
        int len = a.length;
        while (a[len-1] != b){
            len--;
        }
        if (len==1){
            System.out.println("没找到");
        } else {
            System.out.println(len-2);
        }
    }

    private static void Find1(int[] a, int b) {
        for (int i = 0; i < a.length; i++) {
            if (a[i] == b){
                System.out.println(i);
                break;
            }
        }
    }
}


有序表查找

折半查找

思路:前提是线性表中的关键词有序,线性表示顺序结构。取中间记录作为比较对象,若给定值小于中间值,就在左边区间找;若大于中间值,就在右边区间找。

代码实现

int Binary_Search(int *a, int n, int key)
{
    int low, high, mid;
    low = 1;    // 最低下标为记录首位
    high = n;    // 最高下标为记录末位
    while (low <= high)
    {
        mid = (low + high) / 2;    // 折半
        if (key < a[mid])
            high = mid - 1;
        else if (key > a[mid])
            low = mid + 1;
        else
            return mid;    // 相等表示找到
    }
    return 0;
}

插值查找

思路:优化二分查找法,根据key在数值域中大小比例,来确定在哪找。

推导

m i d = l o w + h i g h 2 = l o w + 1 2 ( h i g h − l o w ) = l o w + k e y − a [ l o w ] a [ h i g h ] − a [ l o w ] ( h i g h − l o w ) mid = \frac{low + high}{2}=low + \frac{1}{2}(high-low)=low+\frac{key-a[low]}{a[high] - a[low]}(high-low) mid=2low+high=low+21(highlow)=low+a[high]a[low]keya[low](highlow)

核心代码

mid = low + (high-low)*(key-a[low]) / (a[high]-a[low]);

斐波那契查找(没理解)

找数组的长度在F数组中的位置。
根据F中的数字来扩充a数组,后面的值用a数组中的最大值填充
mid的值是由F决定的,mid=low+F[k-1] - 1
比较后修改high,low,k// 为什么要这样改k呢
如果小了,k-1,大了k-2.high并不会影响mid的选择,low才会

k在逐渐变小,得到的F值也在变小,F值是a的下标,如果key比a大,那么low变大了,F也会变得很大,此时k-2的话,得到的F值仍然是大的,

没看懂啊,后面再看吧

int Fibonacci_Search(int *a, int n, int key)
{
    int low, high, mid, i, k;
    low = 1;    // 最低小标为记录首位
    high = n;
    k = 0;
    while (n > F[k]-1)    // 计算n位于斐波那契数列的位置
        k++;
    for (i=n; i<F[k]-1; i++)
        a[i] = a[n];
    while (n > F[k]-1)    // 看看长度位于F数组的什么位置
        k++;
    for (i=n; i<F[k]-1; i++)    // a数组的长度顺着上面F的取值,要扩充
        a[i] = a[n];
    while (low <= high)
    {
        mid = low + F[k-1] - 1;    // 计算当前分割下标
        if (key < a[mid])
        {
            high = mid-1;
            k = k-1;
        }
        else if (key > a[mid])
        {
            low = mid+1;
            k = k-2;
        }
        else
        {
            if (mid <=n )
                return mid;
            else
                return n;
        }
    }
    return 0;
}

JAVA实现有序表查找

折半

public class BinarySearch {
    public static void main(String[] args) {
        int[] a = {1, 2, 3, 4, 5, 6, 7, 8};
        int key = 3;
        BinarySc(a, key);
    }

    private static void BinarySc(int[] a, int key) {
        int high = a.length-1;
        int low = 0;
        int min = 0;
        while (low <= high){
            min = (low + high) / 2;
            if (a[min] > key){
                high = min - 1;
            } else if (a[min] < key){
                low = min + 1;
            } else {
                System.out.println(min);
                break;
            }
        }
    }
}

线性索引查找

稠密索引

稠密索引:数据集中每个记录都有一个索引,索引项一定按照关键码有序排列。


分块索引

分块有序:将数据集分块,按块给索引,这些块要满足下面两个条件,这样的序列叫做分块有序:

  • 块内无序:每个块内的元素不需要有序
  • 块间有序:比如第二块的记录的关键字均大于第一块中所有记录的关键字。

分块索引表结构

  • 最大关键码,存储每一个块中的最大关键字,好处是可使下一块的最小关键字也能比上一块最大的关键字大。
  • 存储了块中的记录个数,以便循环时使用。
  • 用于指向首数据元素的指针,便于开始对这一块中记录进行遍历。

分块索引表查找步骤

  1. 先用简单的算法找到位于哪个块
  2. 然后利用块的指针, 在块中顺序搜索即可

时间复杂度分析
共有n个记录,设有m块,每块t条记录,所以块的查找假设为 m + 1 2 \frac{m+1}{2} 2m+1次,
块中的查询设为 t + 1 2 \frac{t+1}{2} 2t+1次,所以总查找为:

m + 1 2 + t + 1 2 = 1 2 ( m + t ) + 1 = 1 2 ( n t + t ) + 1 \frac{m+1}{2} + \frac{t+1}{2} = \frac{1}{2}(m+t) +1 = \frac{1}{2}(\frac{n}{t} + t) +1 2m+1+2t+1=21(m+t)+1=21(tn+t)+1

最好的情况是m与t相等,所以次数为 n + 1 \sqrt{n}+1 n +1,时间复杂度为 O [ n ] O[\sqrt{n}] O[n ],比折半查找的 O [ log ⁡ n ] O[\log n] O[logn]差不少。


倒排索引

索引项的通用结构

  • 次关键码,例如上面的英文单词
  • 记录号表,例如上面的文章编号

倒排索引:就是和上面的分块索引相反,左边放元素,右边放在哪个块。记录号表存储具有相同次关键码的所有记录的记录号,这样的索引方法就是倒排索引。
因为生活中有时需要根据属性来查找记录,例如搜索引擎。


二叉排序树

定义:二叉排序树(Binary Sort Tree),又称为二叉查找树,它或者是一颗空树,或者是有下列性质的二叉树:

  • 若左子树不为空,则左子树上所有的结点都小于根结点
  • 若右子树不为空,则右子树上所有的结点都大于根结点
  • 左右子树也分别为二叉排序树
  • 使用中序遍历可得从小到大的序列

作用:并不是为了排序,而是为了提高查找和插入删除关键字的速度。

二叉树结构

typedef struct BiTNode    // 结点结构
{
    int data;
    struct BitNode *lchild, *rchild;
} BiTNode, *BitTree;

二叉排序树的查找

思路:f用来指向双亲,p用来保存结果,找到了就为此结点,到最后一直没找到就返回离该点最接近的一个结点。

代码实现

Status SearchBST(BiTree T, int key, BiTree f, BiTree *p)
{
    if (!T)    // 大小比较完的最后,到了null就表示找不到了
    {
        *p = f;    // 返回空的父结点,就是二叉树中最接近的一个顶点
        return FALSE:
    }
    else if (key == T->data)
    {
        *p = T;
        return TRUE;
    }
    else if (key < T->data)
        return SearchBST(T->lchild, key, T, p);    // 在左子树继续找
    else
        return SearchBST(T->rchild, key, T, p);    // 在右子树继续找
}

二叉排序树插入操作

思路:先检查树里有没有和插入点重复的,有就不插,没有就找到最接近插入点的结点,根据该结点和插入点的大小关系来判断为左孩子还是右孩子。

代码实现

Status InsertBST(BiTree *T, int key)
{
    BiTree p, s;
    if (!SearchBST(*T, key, NULL, &p))    // 没找到就添加
    {
        // 先把结点建好
        s = (BiTree) malloc(sizeof(BiTNode));
        s->data = key;
        s->lchild = s->rchild = NULL;
        // 添加环节
        if (!p)    // 如果是个空树,就把插入的点当成根结点
            *T = s;
        else if (key < p-data)
            p->lchild = s;    // 插入s为左孩子
        else
            p->rchild = s;
        return TRUE;
    }
    else
        return FALSE;    // 已经有了相同的关键点,不再插入
}

二叉排序树删除操作

思路: 找删除点的中序前驱结点来替换被删除点,前驱和该删除点在排序上是相邻,所以是最适合替换的,同理也可用后驱替换。所以应有三个元素,一是被删除点,二是前驱点,三是前驱的父结点,前驱的父结点用来把断的接上。

查找代码
删除前要找到结点,找到后针对结点删除

Status DeleteBST(BiTree *T, int key)
{
    if (!*T)    // 不存在的话
        return FALSE;
    else
    {
        if (key == (*T)->data)    // 找到关键字等于key的数据元素
            // 此处和查找不同
            return Delete(T);
        else if (key < (*T)->data)
            return DeleteBST(&(*T)->lchild, key);
        else
            return DeleteBST(&(*T)->rchild, key);
    }
}

删除代码

Status Delete(BiTree *p)
{
    BiTree q, s;
    if ((*p)->rchild == NULL)    // 右子树空则只需要重接左子树
    {
        q = *p;
        *p = (*p)->lchild;
        free(q);
    }
    else if ((*p)->lchild == NULL)    // 同理
    {
        q = *p;
        *p = (*p)->rchild;
        free(q);
    }
    else    // 左右子树均不为空
    {
        q = *p;
        s = (*p)->lchild;
        // 找p的左孩子的右孩子的尽头,s
        while (s->rchild)    // s指向被删除结点的前驱,q指向s的父结点
        {
            q = s;
            s = s->rchild;
        }
        (*p)->data = s->data;    // 将前驱的值赋给被删除结点位置
        // 当q的左孩子拥有右孩子的时候
        if (q != *p)
            q->rchild = s->lchild;    // 重接q的右子树
        // 当q的左孩子没有右孩子的时候,q=p,没动
        else
            // 删除点的左孩子接替删除点的位置
            // 正因没有右孩子,所以不会有影响
            q->lchild = s->lchild;
        free(s);
    }
    return TRUE;
}

删除操作图示
绿色的线表示结点的变动,蓝色的数字表示中序遍历顺序,黄色表示结点


二叉排序树总结

二叉树虽然插入删除比顺序表简单,但也存在问题,树的结构是很影响速度的。
同样的数据元素,不同的排列顺序,会有不同的树的结构:

查找结点99,左边只需比较两次,而右边需要比较10次。
所以希望二叉排序树是比较平衡的,深度与完全二叉树相同,均为 [ log ⁡ 2 n ] + 1 [\log_2n]+1 [log2n]+1,所以茶中的复杂度也为 O [ log ⁡ n ] O[\log n] O[logn]


JAVA实现二叉排序树查找、插入、删除

因为java中,没有Tree实例就不能操作,没有指针,所以定义了一个变量,来标记该结点是否存在。

结点类

public class Tree {
    private int data;
    // 因为java中,没有Tree实例就不能操作,没有指针
    // 所以再定义一个变量,来标记该结点是否存在
    public boolean exist=false;
    public Tree lchild, rchild;

    public int getData() {
        return data;
    }

    public void setData(int data) {
        exist = true;
        this.data = data;
    }

    public Tree() {
        lchild = rchild = null;
    }

    public Tree(int data) {
        exist = true;
        this.data = data;
        lchild = rchild = null;
    }

    public void equal(Tree t){
        if (t != null){
            this.exist = true;
            this.data = t.getData();
            this.lchild = t.lchild;
            this.rchild = t.rchild;
        } else {
            this.exist = false;
        }

    }
}

测试类

public class BaseBT {
    public static void main(String[] args) {
        int[] a = {62, 58, 88, 47, 73, 99, 35, 52, 93, 37};
        Tree T = new Tree();
        // 创建二叉排序树,就是一直插入的过程
        for (int i = 0; i < a.length; i++) {
            SearchOrInsert(T, a[i], null, false);
        }
        System.out.println("中序遍历--------");
        // 中序遍历即可得到升序
        TraverseTree(T);
        // 删除结点
        int key = 52;
        System.out.println("删除的值为:" + key);
        DeleteNode(T, key);
        System.out.println("中序遍历--------");
        TraverseTree(T);
    }

    private static void DeleteNode(Tree t, int key) {
        // 首先要找到位置
        if (t.exist == false){
            System.out.println("无此元素");
        } else {
            if (key == t.getData()){
                Delete(t);
            } else if (key < t.getData()){
                DeleteNode(t.lchild, key);
            } else {
                DeleteNode(t.rchild, key);
            }
        }
    }

    private static void Delete(Tree t) {
        // 如果左右孩子存在空,是最简单的
        // 如果左孩子为空,那么可能就是只有右孩子或都没有
        if (t.lchild == null){
            t.equal(t.rchild);
        } else if (t.rchild == null){
            t.equal(t.lchild);
        } else {
            // 找前驱
            Tree q = t;
            // 先找一个左孩子
            Tree s = q.lchild;
            // 再一直找左孩子的右孩子
            while (s.rchild!=null){
                q = s;
                s = s.rchild;
            }
            // 此时可以确定t的新值
            t.setData(s.getData());
            // 此时要看s是否有右孩子,没有就直接接上
            if (q == t){
                // 如果左孩子没有右孩子,那么t的值就是左孩子,所以去掉左孩子
                t.lchild = s.lchild;
            } else {
                q.rchild.equal(s.lchild);
            }
        }
    }

    private static void TraverseTree(Tree t) {
        if (t!=null && t.exist){
            TraverseTree(t.lchild);
            System.out.println(t.getData());
            TraverseTree(t.rchild);
        }
    }

    // 查询插入一体化
    private static boolean SearchOrInsert(Tree t, int i, Tree f, boolean search){
        // 考虑根结点为空的情况
        // tree不为空但exits为false,只有这一种情况
        if (t!=null && t.exist==false){
            System.out.println("插入了:" + i);
            t.setData(i);
            return false;
        }

        // t是树的根结点,i是被查找元素,f是当前结点的父结点
        if (t == null){
            if (search){
                System.out.println("没找到");
            } else {
                System.out.println("插入了:" + i);
                if (i > f.getData()){
                    f.rchild = new Tree(i);
                } else {
                    f.lchild = new Tree(i);
                }
            }
            return false;
        }
        // 找到了
        if (t.getData() == i){
            System.out.println("找到了");
            return true;
        }
        // 往右子树走
        if (t.getData() < i){
            return SearchOrInsert(t.rchild, i, t, search);
        } else {
            return SearchOrInsert(t.lchild, i, t, search);
        }
    }
}

平衡二叉树AVL

定义:平衡二叉树是一种二叉排序树,其中每一个结点的左子树和右子树的高度差至多等于1。

平衡因子BF:将二叉树上结点的左子树深度减去右子树深度的值称为平衡因子BF。

例子

最小不平衡子树:距离插入结点最近的,且平衡因子的绝对值大于1的结点为根的子树,称为最小不平衡子树。

案例
插入了37,58的高度变成了2,BF也变成了2。


平衡二叉树实现原理

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

案例:数组为{3,2,1,4,5,6,7,10,9,8},最终结果为下图,步骤就不上了,太多了。

方法

  • 出现不平衡问题的时候要立即修正
  • 如果最小不平衡树的根结点为负数,该最小不平衡树就左旋,正数就右旋
  • 如果最小不平衡树的根结点和孩子结点的BF符号不一样,就得调整到符号一样,调整的方法可能是改变顺序(11和12),也可能是旋转(14和15)




平衡二叉树实现算法

改进结点,添加BF因子

typedef struct BiTNode    // 结点结构
{
    int data;
    int bf;
    struct BiTNode *lchild, *rchild;
} BiTNode, *BiTree;

旋转可行的原因
二叉排序树的构建和元素的输入顺序有很大关系,而下面的旋转操作并不像影响排序的正确性,所以旋转相当于把输入的顺序重新排了,数还是那些数,中序遍历起来也还是升序的,所以没问题。

右旋

  • 让原本根结点的左孩子作新的根结点,原本根结点作为新根结点的右孩子,重点是,旋转后的中序遍历顺序不能变。
  • BF在别的地方改变,所以不需要在这里考虑BF
// 对以p为根的二叉排序树作右旋
// 之后p指向新的结点
void R_Rotate(BiTree *p)
{
    BiTree L;
    L = (*p)->lchild;
    // 可能存在,也可能不存在
    // 如果存在的话,为了保证正确的的大小顺序,具体看下图
    (*p)->lchild = L->rchild;
    // 重点就在这,让p的左孩子成为新的根结点
    L->rchild = (*p);
    *p = L;
}

右旋中的排序理解
红色为BF,蓝色为中序顺序,注意,此处的BF只是一个参考。
右旋

右旋案例

左旋

void L_Rotate(BiTree *p)
{
    BiTree R;
    R = (*p)->rchlid;
    (*p)->rchild = R->lchild;
    R->lchild = (*p);
    *p = R;
}

左平衡旋转处理
思路:这里已经知道是要处理左平衡,所以知道T的BF大于0,直接从根结点的左孩子下手,先判断左孩子的BF,如果是同号,那么做简单的右旋并修改各个结点的BF即可;如果是异号,则以左孩子为根结点左旋,变为正BF,再对根结点右旋,同时修改各个结点的BF,这里的BF修改还跟插在了哪颗子树相关。

下图算是比较清晰例子:

#define LH +1    // 左高
#define EH 0    // 等高
#define RH -1    // 右高
// 对T所指结点为根的二叉树作左平衡旋转处理
// 结束时T指向新的根结点
void LeftBalance(BiTree *T)
{
    BiTree L, Lr;
    L = (*T)->lchild;    // 处理左平衡,所以直接左子树
    // 判断同号还是异号
    switch(L->bf)
    {
        case LH:    // 同号,新点插在了T的左孩子的左子树上,做单右旋处理
            (*T)->bf = L->bf = EH;
            R_Rotate(T);
            break;
        case RH:    // 异号,插在了左孩子的右子树上,做双旋处理
            Lr = L->rchild;    // 左孩子的右子树根
            // 判断是在右子树的何处,借此修改各结点bf
            // 不过=0没看懂
            switch(Lr->bf)
            {
                case LH:
                    (*T)->bf = RH;
                    L->bf = EH;
                    break;
                case EH:
                    (*T)->bf = L->bf = EH;
                    break;
                case RH:
                    (*T)->bf = EH;
                    L->bf = LH;
                    break;
            }
            Lr->bf = EH;
            L_Rotate(&(*T)->lchild);    // 对T的左子树做左旋
            R_Rotate(T);    // 对T做右旋
    }
}

左平衡的三种case图例
对应异号部分的三种情况,蓝色为中序顺序,红色为BF,N为新插入的点,在EH情况下,Lr就是N。

  1. EH
  2. LH
  3. RH

左平衡规律总结

  1. Lr会成为新的根结点,且bf为0。
  2. 因为是左平衡且异号,所以变化很固定,先根结点的左孩子左旋,再根结点右旋。
  3. T和L的bf和孩子取决于,N是Lr的左孩子还是又孩子。如果是左孩子,那么会小于根结点Lr,所以就会分配给L,所以此时L的bf=0,T的bf=-1。
  4. 如果N是Lr的右孩子,那么会大于根结点Lr,就会分配给T,所以此时L的bf=1,T的bf=0。

主函数
思路

  • 先说插入:这是一个插入函数,每次插入调用一次。想插入就得先找到位置,所以该函数用了递归来寻找位置,使用InsertAVL(&(*T)->lchild...来递归查找,如果输入的参数T变成了Null,说明找到了位置,即可在该指针处创建结点。
  • 再说平衡:由if (e<(*T)->data)这个判断可知道,插入后紧接着就会检查新插入结点的父结点的BF,并根据BF的值来判断是否需要对父结点进行平衡并该BF操作,不需要的话就把父结点的BF改了,毕竟插入了,所以一定会变。如果原来的父结点的BF为0的话,此时高度就会变,所以taller会变成TRUE,此时回到递归的上一次,即父结点为T的时候,此时因为taller变了,所以还要再判断是否平衡。因此只要高度变了,就会从下往上根据判断条件平衡。从下往上的话即不会漏掉,也可以在第一时间平衡。
  • 这里面最近T是被插入的结点的父节点,然后T会一层一层再往上移动,然后层层改变BF。
  • 做了左、右平衡后,taller=False,此时就不需要再网上判断了,所以说每插入一次,最多做一次平衡就行了
// 若不存在和e相同的,则插入并返回1,否则返回0
// 若插入后使二叉排序树失去平衡,则作平衡旋转处理
// taller反应是否长高
Status InsertAVL(BiTree *T, int e, Status *taller)
{
    if (!*T)
    {
        // 插入新结点,树长高
        *T = (BiTree) malloc(sizeof(BiTNode));
        (*T)->data = e;
        (*T)->lchild = (*T)->rchild = NULL:
        (*T)->bf = EH;
        *taller = TRUE;
    }
    else
    {
        if (e == (*T)->data)
        {    // 已有,不再插入
            *taller = FALSE;
            return FALSE;
        }
        if (e < (*T)->data)
        {   // 在T的左子树继续搜索
            if (!InsertAVL(&(*T)->lchild, e, taller))
                // 插入失败
                return FALSE;
            // 长高了,就得修改BF并考虑平衡问题
            if (taller)
            {
                switch((*T)->bf)
                {
                    case LH:    // 原来为左高,左边再加一个就不平衡了
                        LeftBalance(T);
                        *taller = FALSE;
                        break;
                    case EH:    // 原来一样高
                        (*T)->bf = LH;
                        *taller = TRUE;
                        break;
                    case RH:
                        // 为什么这个也有False?
                        // 左右变平衡,说明是在短的一边加的,所以高度不变
                        (*T)->bf = EH;
                        *taller = FALSE;
                        break;
                }
            }
        }
        else
        {
            if (!InsertAVL(&(*T)->rchild, e, taller))
                return FALSE;
            if (taller)
            {
                switch((*T)->bf)
                {
                    case LH:
                        (*T)->bf = EH;
                        *taller = FALSE;
                        break;
                    case EH:
                        (*T)->bf = RH;
                        *taller = TRUE;
                        break;
                    case RH:
                        LeftBalance(T);
                        *taller = FALSE;
                        break;
                }
            }
        }
    }
    return TRUE;
}

生成平衡二叉树

int i;
int a[10] = {...};
BiTree T = NULL;
Status taller;
for (i=0; i<10; i++)
{
    InsertAVL(&T, a[i], &taller);
}

时间复杂度
查找、删除、插入都是 O [ log ⁡ n ] O[\log n] O[logn]


JAVA实现平衡二叉树相关

结点类

public class BitNode {
    public int BF;
    public BitNode lchild, rchild;
    private int data;
    public boolean exist=false;

    public int getData() {
        return data;
    }

    public void setData(int data) {
        exist = true;
        this.data = data;
    }



    public BitNode() {
        lchild = rchild = null;
    }

    public BitNode(int data) {
        exist = true;
        this.data = data;
        lchild = rchild = null;
    }

    public void equal(BitNode bitNode){
        if (bitNode!=null)
        {
            exist = bitNode.exist;
            data = bitNode.data;
            BF = bitNode.BF;
            lchild = bitNode.lchild;
            rchild = bitNode.rchild;
        } else {
            exist = false;
        }
    }
}

方法类

public class AVLutils {
    public static void L_Rotate(BitNode T) {
        // T的分身
        BitNode L = new BitNode();
        L.equal(T);
        // T的右孩子上位新跟结点
        T.equal(T.rchild);
        L.rchild = T.lchild;
        T.lchild = L;
    }

    public static void R_Rotate(BitNode T) {
        // T的分身
        BitNode L = new BitNode();
        L.equal(T);
        // T的右孩子上位新跟结点
        T.equal(T.lchild);
        L.lchild = T.rchild;
        T.rchild = L;
    }

    public static void LeftBalance(BitNode T){
        // 做平衡,T.bf=1,是在变化之前传进函数的
        BitNode L = T.lchild;
        // 检查同号或异号
        switch (L.BF){
            // 同号的情况
            case 1:
                // 右转T
                R_Rotate(T);
                T.BF = L.BF = 0;
                break;
            // 异号的情况,这里要考虑Lr的符号
            case -1:
                BitNode Lr = L.rchild;
                switch (Lr.BF){
                    // 插在了Lr的右边,此时新结点跟T走
                    case -1:
                        T.BF = 0;
                        L.BF = 1;
                        break;
                    // 插在了Lr的左边,此时新结点跟L走
                    case 1:
                        T.BF = -1;
                        L.BF = 0;
                        break;
                    case 0:
                        T.BF = L.BF = 0;
                        break;
                }
                // 根据规律可得以下固定内容
                Lr.BF = 0;
                L_Rotate(L);
                R_Rotate(T);
        }
    }

    public static void RightBalance(BitNode T){
        BitNode R = T.rchild;
        switch (R.BF){
            // 此时同号为负
            case -1:
                L_Rotate(T);
                T.BF = R.BF = 0;
            case 1:
                BitNode Rl = R.lchild;
                switch (Rl.BF){
                    case 0:
                        T.BF = R.BF = 0;
                    // 插在了左边,跟着T
                    case 1:
                        T.BF = 0;
                        R.BF = -1;
                    case -1:
                        T.BF = 1;
                        R.BF = 0;
                }
                Rl.BF = 0;
                R_Rotate(R);
                L_Rotate(T);
        }
    }

    // 通过比较来找位置
    public static boolean InsertAVL(BitNode T, BitNode f, int e, Status sta){



        // 不能存在说明找到要插入的位置了,假设只要插入了就变高
        // 其实就是个检查机制,有插入就检查
        if (T==null){

            // 父结点不为空
            if (e > f.getData()){
                f.rchild = new BitNode(e);
            } else {
                f.lchild = new BitNode(e);
            }
            System.out.println("插入结点:" + e);
            sta.taller = true;
            return true;
        } else {
            // f表示父结点,f和T都为空说明是根结点
            if (f==null && T.exist==false){
                T.setData(e);
                System.out.println("插入结点:" + e);
                return true;
            }

            if (e == T.getData()){
                System.out.println("重复了:" + e);
                sta.taller = false;
                return false;
            } else if (e > T.getData()){
                // 往右子树找
                // 如果插入失败,则跳过
                if (!InsertAVL(T.rchild, T, e, sta)){
                    return false;
                }
                // 插入成功后,检查高度
                if (sta.taller){
                    // 现在是插到了右边
                    switch (T.BF){
                        case 0:
                            // 插入打破了平衡,说明最高高度变了
                            T.BF = -1;
                            sta.taller = true;
                            break;
                        case 1:
                            // 左右变平衡,说明是在短的一边加的,所以最高高度不变
                            T.BF = 0;
                            sta.taller = false;
                            break;
                        case -1:
                            RightBalance(T);
                            sta.taller = false;
                            break;
                    }
                }
            } else {
                // 往左子树找
                if (!InsertAVL(T.lchild, T, e, sta)){
                    return false;
                }
                if (sta.taller){
                    switch (T.BF){
                        case 0:
                            // 插入打破了平衡,说明最高高度变了
                            sta.taller = true;
                            T.BF = 1;
                            break;
                        case -1:
                            // 左右变平衡,说明是在短的一边加的,所以最高高度不变
                            sta.taller = false;
                            T.BF = 0;
                            break;
                        case 1:
                            LeftBalance(T);
                            sta.taller = false;
                            break;
                    }
                }
            }
        }
        // 能走到这里就说明插入成功了,否则在前面就返回false了
        return true;
    }

    public static void TraverseTree(BitNode T){
        if (T!= null){
            TraverseTree(T.lchild);
            System.out.println(T.getData());
            TraverseTree(T.rchild);
        }
    }
}

高度标记类

public class Status {
    public boolean taller;

    public Status(boolean taller) {
        this.taller = taller;
    }
}

测试类

public class main {
    public static void main(String[] args) {
        int[] a = {62, 58, 88, 47, 73, 99, 35, 52, 93, 37};
        BitNode T = new BitNode();
        Status sta = new Status(false);
        for (int i = 0; i < a.length; i++) {
            AVLutils.InsertAVL(T, null, a[i], sta);
        }
        System.out.println("中序遍历-------------");
        AVLutils.TraverseTree(T);
    }
}

多路查找树(B树)

多路查找树(mutil-way search tree):其每一个结点的孩子数可以多与两个,且每一个结点处可以存储多个元素。
由于它是查找树,所有元素之间存在某种特定的排序关系。

2-3树

2-3:每一个结点都有两个孩子(称它为2结点)或三个孩子(称它为3结点)

2结点:包含一个元素和两个孩子,排序和二叉排序树类似,但2结点要么没有孩子,要么有两个孩子。

3结点:包含一大一小两个元素和三个孩子,左子树包含小于较小元素的元素,中间子树包含介于较小较大之间的元素,右子树包含大于较大元素的元素。3结点要么没有孩子,要么有两个孩子。

性质:2-3树中所有叶子都在同一层次。

2-3树的插入

  1. 空树插入2结点即可
  2. 插入元素到2结点中。如下图所示,3介于1,4之间,将左下角的2结点1改为3结点1、3。
  3. 插入元素到3结点中。此时3结点元素已经满了,所以要求修改3结点的父结点,把父结点改造成3结点。

    4.插入元素到3结点的其他情况。如果父结点已经是3结点,那就继续找父结点的父结点,直到找到2结点。

2-3树的删除

  1. 删除3结点上的叶子结点,直接和删除即可
  2. 删除2结点的叶子结点,可能会破坏2结点的定义,有四种情况在后面介绍
  3. 所删除的元素位于非叶子的分支结点,通常是将树按中序遍历后得到此元素的前驱或后继,考虑让他们补位

删除2结点的叶子结点四种情况

  1. 双亲是2结点,且拥有3结点的右孩子
  2. 双亲是2结点,右孩子也是2结点
  3. 双亲是一个3结点
  4. 如果当前树是一个满二叉树的情况,此时删除任何一个结点都不会满足2-3结点的定义

2-3-4树

概念:2-3树的拓展,包含了4结点的使用,包含大中小三个元素和四个孩子,也是要么四个要么没有。然后根据三个数分成四个区间,对应区间的数分配到对应的子树中。

案例
数组:{7,1,2,5,6,9,8,4,3}。
创建流程:

删除流程,删除顺序是1、6、3、4、5、2、9:


B树

B树:B树是一种平衡的多路查找树,2-3树和2-3-4树都是B树的特例。

B树的阶:结点最大的孩子数目。所以2-3树是3阶B树,2-3-4树是4阶B树。

一个m阶的B树有以下属性

  • 如果根结点不是叶结点,则至少有两棵子树。
  • 每一个非根的分支结点都有k-1个元素和k个孩子,其中 [ m / 2 ] ≤ k ≤ m [m/2] \le k \le m [m/2]km;每一个叶结点n都有k-1个元素,其中 [ m / 2 ] ≤ k ≤ m [m/2] \le k \le m [m/2]km
  • 所有叶子结点都位于同一层次。

B+树(没看)

这个没代码也不知道如何用,没耐心看,先跳过


散列表(哈希表)查找概述

散列技术:在记录的存储位置和它的关键字之间建立一个确定的对应关系f,使得每个关键字key对应一个存储位置f(key)。

散列函数/哈希函数:把这种对应关系f称为散列函数,又称哈希函数。

散列表/哈希表:采用散列技术将记录存储在一块连续的存储空间中,这块连续存储空间称为散列表或哈希表。

散列表查找步骤

  1. 根据散列函数计算地址,然后按地址存储该记录。
  2. 查找记录时,根据同样的散列函数计算记录的地址,直接访问地址。

适合场景:适合的求解问题是查找与给定值相等的记录。

冲突 key 1 ≠ key 2 \text{key}_1 \not = \text{key}_2 key1=key2的时候,却又 f ( key 1 ) = f ( key 2 ) f(\text{key}_1) = f(\text{key}_2) f(key1)=f(key2),这种现象称为冲突,把这两个关键字称为这个散列函数的同义词。


散列函数的构造方法

散列函数的设计原则

  1. 计算简单,计算时间不应超过其他查找技术与关键字比较的时间。
  2. 散列地址分布均匀,让散列地址均匀分布在存储空间中,保证存储空间的有效利用。

直接定址法

取关键词的某个线性函数值为散列地址:

f ( k e y ) = a x k e y + b ( a 、 b 为 常 数 ) f(key) = a x key + b (a、b为常数) f(key)=axkey+b(ab)
使用场景:需事先知道关键字分布情况,适合查找表较小且连续的情况,由于这样的限制,并不常用。

数组分析法

抽取一部分,再进行反转、移位、叠加等操作。
使用场景:处理关键字位数较大的情况,如果事先知道关键字的分布且关键字的若干位分布较均匀,就可以考虑用这个方法。

平方取中法
假设关键字是1234,平方就是1522756,取中间三位是227,用作散列地址,也可以是275。
使用场景:不知道关键字的分布,而位数又不是很大的情况。

折叠法
从左到右分割成位数相等的几部分,最后一部分位数不够可以短些,然后将这几部分叠加求和,并按散列表表长,取后记为做散列地址。
比如关键字是9876543210,散列表表长为3为,分成四组,987、654、321、0,然后叠加求和987+654+312+0=1962,再求后三位得散列地址962。
此时还不能保证分布均匀,可以折叠后再相加,如变成789+654+123+0=1566,此时地址为566。
使用场景:事先不需要知道关键字的分布,适合关键字位数较多的情况。

除留余数法
对于散列表长为m的散列函数公式:

f ( k e y ) = k e y m o d p ( p 小 于 等 于 m ) f(key)=key mod p (p小于等于m) f(key)=keymodp(pm)
mod是取模的意思,不仅可以对关键字直接取模,也可在折叠、平方取中后再取模。
比如 29 mod 12 = 5,就放在下标为5的位置:

使用经验:若表长为m,通常p为小于或等于表长(最好接近m)的最小质数或不包含小于20质因子的合数。

随机数法

f ( k e y ) = r a n d o m ( k e y ) f(key)=random(key) f(key)=random(key)

不同方法的考虑因素

  1. 计算地址所需时间
  2. 关键字的长度
  3. 散列表的大小
  4. 关键字的分布情况
  5. 记录查找的频率

处理散列冲突的方法

开放定址法
一旦发生了冲突,就去寻找下一个空的散列地址,只要散列表足够大,空的散列地址总能找到,并将记录存入。
冲突了就用下面的公式找新地址:

f i ( k e y ) = ( f ( k e y ) + d i ) M O D m ( d i = 1 , 2 , 3 , . . . , m − 1 ) f_i(key) = (f(key) +d_i) MOD m (d_i=1,2,3,...,m-1) fi(key)=(f(key)+di)MODm(di=1,2,3,...,m1)

上面是线性探测法,但是会出现本来不是同义词(第一次f的时候得到的值不相同)却要争夺一个地址的情况,这种现象称为堆积,但是堆积会大大降低存入和查找的效率。

因此提出二次探测法,目的是为了不让关键字都聚集在某一块区域:

f i ( k e y ) = ( f ( k e y ) + d i ) M O D m ( d i = 1 2 , − 1 2 , 2 2 , − 2 2 , . . . , q 2 , − q 2 , q ≤ m / 2 ) f_i(key) = (f(key) +d_i) MOD m (d_i=1^2,-1^2,2^2,-2^2,...,q^2, -q^2, q \le m/2) fi(key)=(f(key)+di)MODm(di=12,12,22,22,...,q2,q2,qm/2)

还有一种随机探测法,因为可设定随机数种子,所以随机数不会重复:

f i ( k e y ) = ( f ( k e y ) + d i ) M O D m ( d i 是 一 个 随 机 数 列 ) f_i(key) = (f(key) +d_i) MOD m (d_i是一个随机数列) fi(key)=(f(key)+di)MODm(di)

再散列函数法
准备多个散列函数一起用,每当发生散列地址冲突的时候,就换一个计算方法,这样做消耗的时间比较多:

f i ( k e y ) = R H i ( k e y ) ( i = 1 , 2 , . . . , k ) f_i(key) = RH_i(key) (i=1,2,...,k) fi(key)=RHi(key)(i=1,2,...,k)

链地址法
将所有关键字为同义词的记录存储在一个单链表中,我们称这种表为同义词子表,在散列表中值存储所有同义词子表的头指针。
该方法一定会为元素提供地址,但是会多出查找时遍历单链表的性能损耗。
例如集合{12,67,56,16,25,37,22,29,15,47,48,34},以12为出书,得表:

公共溢出区法
创建一个地方专门存放冲突的关键字。

使用场景:适合冲突数据很少的情况,该结构对查找很友好。


散列表查找实现

定义散列表结构

#define SUCCESS 1
#define UNSUCCESS 0
#define HASHSIZE 12 // 定义散列表长为数组的长度
#define NULLKEY -32768
typedef struct HashTable
{
    int *elem;    // 元素存储基址,动态分配数组
    int count;    // 当前数据元素个数
}
int m=0;    // 散列表表长,全局变量

散列表的初始化

Status Init HashTable(HashTable *H)
{
    int i;
    m = HASHSIZE;
    H->count = m;
    H->elem = (int *)malloc(m * sizeof(int));
    for (i=0; i<m; i++)
        H->elem[i] = NULLKEY;
    return OK;
}

散列函数

int Hash(int key)
{
    return key % m;    // 除留余数法
}

插入操作
插入的关键字集合:{12、67、56、16、25、37、22、29、15、47、48、34}

void InsertHash(HashTable *H, int key)
{
    int addr = Hash(key);
    while (H->elem[addr] != NULLKEY)    // 不为空,表冲突
        addr = (addr + 1) %m;    // 开放定址法的线性探测
    H->elem[addr] = key;    // 有空后插入关键字
}

查找记录

Status SearchHash(HashTable H, int key, int *addr)
{
    *addr = Hash(key);
    while(H.elem[*addr] != key)    // 不相等则说明发生了冲突
    {
        *addr = (*addr + 1) % m;
        if(H.elem[*addr]==NULLKEY || *addr==Hash(key)
        {
            //如果一直没找到到了空地址,或者地址又转回来了
            return UNSUCCESS;    // 说明关键字不存在
        }
    }
    return SUCCESS;
}

散列表查找性能分析

如果没有冲突的话复杂度就是 O [ 1 ] O[1] O[1],但是冲突是无法避免的,平均查找长度取决于以下几个因素:

  1. 散列函数是否均匀。
  2. 处理冲突的方法。性能比较:链地址法 > 二次探测法 > 线性探测法
  3. 散列表的装填因子。装填因子 α \alpha α=记录个数 / 三列表长度。记录越多, α \alpha α越大,产生冲突的可能性就越大。

总结:通常将散列表的空间设置得比查找集合大,虽然浪费了一定的空间,但是换来查找效率的大大提升。


JAVA实现散列表查找

public class main {
    public static int HashSize = 12;
    public static int NullKey = 65535;
    public static void main(String[] args) {
        // 初始化存储数组
        int[] save = new int[12];
        for (int i = 0; i < HashSize; i++) {
            save[i] = NullKey;
        }
        // 存放数组
        int[] a = {2, 4, 55, 22, 550};
        SaveIntoHash(save, a);
        // 查找数组
        System.out.println("------------开始查找----------");

        int[] b = {2, 4, 55, 22, 550, 11};
        for (int i = 0; i < b.length; i++) {
            SearchInHash(save, b[i]);
        }

    }

    private static void SearchInHash(int[] save, int key) {
        boolean flag = true;
        int add = getHash(key);
        while (save[add]!=key){
            add = (add + 1) % HashSize;
            // 如过转了一圈都没找到
            // 或者说add没有再指向值,说明不是冲突而是不存在
            if (save[add]==NullKey || add==getHash(key))
            {
                System.out.println("数组中不存在:" + key);
                flag = false;
                break;
            }
        }
        if (flag){
            System.out.println("找到了:" + key + " 地址为:" + add);
        }
    }

    private static void SaveIntoHash(int[] save, int[] a) {
        for (int i = 0; i < a.length; i++) {
            int add = getHash(a[i]);
            // 如果发生冲突的话
            while (save[add]!=NullKey){
                add = (add + 1) % HashSize;
            }
            save[add] = a[i];
            System.out.println("地址:" + add + " 值:" + a[i]);
        }
    }

    public static int getHash(int k){
        return k % HashSize;
    }
}

思维导图

在这里插入图片描述

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值