第七章学习小结

第七章学习了如何快速高效有序的查找,查找听起来很普通,没有前面的树啊图啊那么牛,但前面学习的东西终究是一种工具,为这些东西服务的。

顺序查找:没什么好讲的,一个一个查

int SequenceSearch(int a[], int value, int n)
{
    int i;
    for(i=0; i<n; i++)
        if(a[i]==value)
            return i;
    return -1;
}
View Code

折半(二分查找):查找时空间复杂度很优秀,达O(log2n)。但折半查找的前提条件是需要有序表顺序存储,对于静态查找表,一次排序后不再变化,折半查找能得到不错的效率。但对于需要频繁执行插入或删除操作的数据集来说,维护有序的排序会带来不小的工作量,那就不建议使用。

int BinarySearch1(int a[], int value, int n)
{
    int low, high, mid;
    low = 0;
    high = n-1;
    while(low<=high)
    {
        mid = (low+high)/2;
        if(a[mid]==value)
            return mid;
        if(a[mid]>value)
            high = mid-1;
        if(a[mid]<value)
            low = mid+1;
    }
    return -1;
}

//二分查找,递归版本
int BinarySearch2(int a[], int value, int low, int high)
{
    int mid = low+(high-low)/2;
    if(a[mid]==value)
        return mid;
    if(a[mid]>value)
        return BinarySearch2(a, value, low, mid-1);
    if(a[mid]<value)
        return BinarySearch2(a, value, mid+1, high);
}
View Code

分块查找:将n个数据元素"按块有序"划分为m块(m ≤ n)。每一块中的结点不必有序,但块与块之间必须"按块有序";实质上时顺序查找的一种改进。

然后就到了树

现是最普通的二叉排序树:

也叫二叉搜索树,或称二叉排序树Binary Sort Tree)或者是一棵空树,或者是具有下列性质的二叉树:

  1)若任意节点的左子树不空,则左子树上所有结点的值均小于它的根结点的值;

  2)若任意节点的右子树不空,则右子树上所有结点的值均大于它的根结点的值;

  3)任意节点的左、右子树也分别为二叉查找树。

typedef struct BiTNode
{
    int data;
    struct BiTNode *lchild, *rchild;
}BiTNode, *BiTree;

Status SearchBST(BiTree T, int key, BiTree f, BiTree *p)
{
    if(!T){
        *p = f;
        return FALSE;
    }else if(key == T->data){
        *p = T;
        return TRUE;
    }else if(key > T->data){
        return SearchBST(T->rchild, key, T, p);
    }else{
        return SearchBST(T->lchild, key, T, p);
    }
}
View Code

抛去创建不谈,在查找时这种树的时间复杂度很优秀,但插入和删除时还是会达到O(n)的复杂度,于是有了B树;

B树定义:

  B树允许每个节点有M-1个子节点。

  • 根节点至少有两个子节点

  • 每个节点有M-1个key,并且以升序排列

  • 位于M-1和M key的子节点的值位于M-1 和M key对应的Value之间

  • 其它节点至少有M/2个子节点

B+树定义:

  B+树是对B树的一种变形树,它与B树的差异在于:

  • 有k个子结点的结点必然有k个关键码;
  • 非叶结点仅具有索引作用,跟记录有关的信息均存放在叶结点中。
  • 树的所有叶结点构成一个有序链表,可以按照关键码排序的次序遍历全部记录。

接着就是散列表了:

构造方法:

1 直接定址法

    所谓直接定址法就是说,取关键字的某个线性函数值为散列地址,即

    优点:简单、均匀,也不会产生冲突。

    缺点:需要事先知道关键字的分布情况,适合查找表较小且连续的情况。

2 数字分析法

    如果关键字时位数较多的数字,比如11位的手机号"130****1234",其中前三位是接入号;中间四位是HLR识别号,表示用户号的归属地;后四为才是真正的用户号。如下图所示。

    如果现在要存储某家公司的登记表,若用手机号作为关键字,极有可能前7位都是相同的,选择后四位成为散列地址就是不错的选择。若容易出现冲突,对抽取出来 的数字再进行反转、右环位移等。总的目的就是为了提供一个散列函数,能够合理地将关键字分配到散列表的各个位置。

    数字分析法通过适合处理关键字位数比较大的情况,如果事先知道关键字的分布且关键字的若干位分布比较均匀,就可以考虑用这个方法。

3 平方取中法

    这个方法计算很简单,假设关键字是1234,那么它的平方就是1522756,再抽取中间的3位就是227,用做散列地址。

    平方取中法比较适合不知道关键字的分布,而位数又不是很大的情况。

4 折叠法

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

    比如关键字是9876543210,散列表表长为三位,将它分为四组,987|654|321|0,然后将它们叠加求和987 + 654 + 321 + 0 = 1962,再求后3位得到散列地址962。

    折叠法事先不需要知道关键字的分布,适合关键字位数较多的情况。

5 除留余数法

    此方法为最常用的构造散列函数方法。对于散列表长为m的散列函数公式为:

    mod是取模(求余数)的意思。事实上,这方法不仅可以对关键字直接取模,也可以再折叠、平方取中后再取模。

    很显然,本方法的关键在于选择合适的p,p如果选不好,就可能会容易产生冲突。

    根据前辈们的经验,若散列表的表长为m,通常p为小于或等于表长(最好接近m)的最小质数或不包含小于20质因子的合数。

6 随机数法

    选择一个随机数,取关键字的随机函数值为它的散列地址。也就是f(key) = random(key)。这里random是随机函数。当关键字的长度不等时,采用这个方法构造散列函数是比较合适的。

 

在构造过程中,两个关键字都争同一个位置咋办?我们又是个和谐社会,这种冲突要处理好

1 开放定址法

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

    它的公式为:

    比如说,关键字集合为{12, 67, 56, 16, 25, 37, 22, 29, 15, 47, 48, 34},表长为12。散列函数f(key) = key mod 12。

    当计算前5个数{12, 67, 56, 16, 25}时,都是没有冲突的散列地址,直接存入,如下表所示。

    计算key = 37时,发现f(37) = 1,此时就与25所在的位置冲突。于是应用上面的公式f(37) = (f(37) + 1) mod 12 =2,。于是将37存入下标为2的位置。如下表所示。

    接下来22,29,15,47都没有冲突,正常的存入,如下标所示。

    到了48,计算得到f(48) = 0,与12所在的0位置冲突了,不要紧,我们f(48) = (f(48) + 1) mod 12 = 1,此时又与25所在的位置冲突。于是f(48) = (f(48) + 2) mod 12 = 2,还是冲突......一直到f(48) = (f(48) + 6) mod 12 = 6时,才有空位,如下表所示。

 

    把这种解决冲突的开放定址法称为线性探测法

    考虑深一步,如果发生这样的情况,当最后一个key = 34,f(key) = 10,与22所在的位置冲突,可是22后面没有空位置了,反而它的前面有一个空位置,尽管可以不断地求余后得到结果,但效率很差。因此可以改进di=12, -12, 22, -22.........q2, -q2(q<= m/2),这样就等于是可以双向寻找到可能的空位置。对于34来说,取di = -1即可找到空位置了。另外,增加平方运算的目的是为了不让关键字都聚集在某一块区域。称这种方法为二次探测法。

    还有一种方法,在冲突时,对于位移量di采用随机函数计算得到,称之为随机探测法

    既然是随机,那么查找的时候不也随机生成di 吗?如何取得相同的地址呢?这里的随机其实是伪随机数。伪随机数就是说,如果设置随机种子相同,则不断调用随机函数可以生成不会重复的数列,在查找时,用同样的随机种子,它每次得到的数列是想通的,相同的di 当然可以得到相同的散列地址。

    总之,开放定址法只要在散列表未填满时,总是能找到不发生冲突的地址,是常用的解决冲突的方法。

2 再散列函数法

    对于散列表来说,可以事先准备多个散列函数。

    这里RH就是不同的散列函数,可以把前面说的除留余数、折叠、平方取中全部用上。每当发生散列地址冲突时,就换一个散列函数计算。

    这种方法能够使得关键字不产生聚集,但相应地也增加了计算的时间。

 

3 链地址法

    将所有关键字为同义词的记录存储在一个单链表中,称这种表为同义词子表,在散列表中只存储所有同义词子表前面的指针。对于关键字集合{12, 67, 56, 16, 25, 37, 22, 29, 15, 47, 48, 34},用前面同样的12为余数,进行除留余数法,可以得到下图结构。

 

 

    此时,已经不存在什么冲突换地址的问题,无论有多少个冲突,都只是在当前位置给单链表增加结点的问题。

    链地址法对于可能会造成很多冲突的散列函数来说,提供了绝不会出现找不到地址的保证。当然,这也就带来了查找时需要遍历单链表的性能损耗。

4 公共溢出区法

    这个方法其实更好理解,你冲突是吧?那重新给你找个地址。为所有冲突的关键字建立一个公共的溢出区来存放。

    就前面的例子而言,共有三个关键字37、48、34与之前的关键字位置有冲突,那就将它们存储到溢出表中。如下图所示。

    在查找时,对给定值通过散列函数计算出散列地址后,先与基本表的相应位置进行比对,如果相等,则查找成功;如果不相等,则到溢出表中进行顺序查找。如果相对于基本表而言,有冲突的数据很少的情况下,公共溢出区的结构对查找性能来说还是非常高的。

大致就这样吧。

自己可能是个学习接收周期比较长的人,一开始的几章总是迷迷糊糊的,小测也惨不忍睹,越往后好像就越好了一点。

前段时间说要养成适合数据结构的属于自己的思维线,现在模模糊糊貌似已经有了,接下来不断打磨吧,弄清晰弄对。

期末考试快来了。。。主要把前面的looklook.相信再回过头看应该会轻松很多。

就是这样~

posted @ 2019-06-02 23:39  我又不乱来aa  阅读(115)  评论(0编辑  收藏  举报