现在的位置: 首页 > 综合 > 正文

每日学习 兜兜转转 6.24

2017年11月22日 ⁄ 综合 ⁄ 共 3359字 ⁄ 字号 评论关闭

此文是转载,随便兜兜转载。

算法导论-散列表

散列表 - 常用的构造散列函数的方法

散列函数能使对一个数据序列的访问过程更加迅速有效,通过散列函数,数据元素将被更快地定位

1、直接寻址表

  当关键字的的全域(范围)U比较小的时,直接寻址是简单有效的技术,一般可以采用数组实现直接寻址表,数组下标对应的就是关键字的值,即具有关键字k的元素被放在直接寻址表的槽k中。直接寻址表的字典操作实现比较简单,直接操作数组即可以,只需O(1)的时间。

2 、散列表
当全域U很大的时候,由于内存的限制,直接寻址表出现问题,因此可以用散列表解决。对于直接寻址表,具有关键字k的元素在槽k中,而该元素则在散列表的h(k)处,其中,h(k)是散列函数,这个函数将全域U映射到散列表T的[0...m-1]的槽位上。对于两个关键字映射到同一个槽位上的情况,我们称之为碰撞。
解决碰撞有:

  • 链接法解决碰撞
  • 开放寻址法

3、散列函数

  好的散列函数的特点是每个关键字都等可能的散列到m个槽位上的任何一个中去,并与其他的关键字已被散列到哪一个槽位无关。多数散列函数都是假定关键字域为自然数N={0,1,2,....},如果给的关键字不是自然数,则必须有一种方法将它们解释为自然数。例如对关键字为字符串时,可以通过将字符串中每个字符的ASCII码相加,转换为自然数。书中介绍了三种设计方案:除法散列法、乘法散法和全域散列法。


1.2.1 将关键字解释为自然数
比如:一个字符串关键字可以被解释为按适当的基数记号表示的整数。如pt表示为(112 * 128) + 116 = 14452。这将字符串看成是以128为基数的整数。
1.2.2 除法散列法
h(k) = k mod m,通过取k除以m的余数来将关键字k映射到m个槽的某一个中去。
注意除数m的选择,m不应该是2的幂,因为如果m = 2^p,则h(k)就是k的p个最低位数字。并且,当k是一个按基数2^p解释的字符串时,选m = 2^p - 1可能是个比较糟糕的选择,因为将k的各字符进行排列并不会改变h(k)的值,这个证明比较简单,略。一般选取m为与2的幂不太接近的素数。
1.2.3 乘法散列法
h(k) = 
⌊m(kA - ⌊kA)⌋。其中A为(0,
1)之间的常数。m一般选为2的某个幂次。Knuth认为A 
≅(√5
- 1)/2 = 0.6180339887…是一个比较理想的值。

4、开放寻址法
在开放寻址法中,所有的元素都存放在散列表中,和连接法处理冲突时采用连接形式不同,开放寻址法的散列表每个表项或包含动态集合的一个元素,或包含NIL。
在开放寻址法中,当要插入一个元素时,可以连续地探查散列表的各项,直到找到一个空槽来放置待插入的关键字为止。
1.3.1 线性探查
h(k, i) = (h'(k) + i) mod m, i = 0, 1, ... ,m - 1
在线性探查方法中,初始探查位置确定了整个序列,因此只有m种不同的探查序列。且它存在一次群集现象。
1.3.2 二次探查

h(k, i) = (h
'(k) + c * i + d * i^2) mod m
其中c和d为辅助常数。初始探查位置为T[h'(k)],后续的探查位置要在此基础上加上一个偏移量,该偏移量以二次的方式依赖于探查号i。它比线性探查好的多。但是如果两个关键字初始探查位置相同,那么他们的探查序列也是相同的。因此它存在程度较一次群集轻的二次群集现象。
1.3.3 双重散列
h(k, i) = (f(k) + i * g(k)) mod m
其中f(k)和g(k)为辅助散列函数。与线性探查和二次探查不同的是,这里的探查序列以两种方式依赖于关键字k。为能查找整个散列表,值g(k)要与m互素。可以取m为2的幂,并设计g(k)为总产生奇数。或者取m为素数,设计g(k)总产生比m小的正整数。比如:可以取m为素数,并取f(k) = k mod m, g(k) = 1 + (k mod n) 其中n略小于m(比如为m - 1)。双重散列法用了
θ(m^2)种探查序列。

每日一C

自引用结构
       结构体内包含指向自身的指针,这类结构体称为自引用结构。常用的链表节点便是自引用结构。
                              struct node{
                       int number;  
                struct node *next;    //指向下一个节点的指针
              };

C语言中,作为参数的数组不能直接传递,将转化为指针传递
           1.参数指针化
           如果使用数组名作为函数参数,该数组名会被转化为指向该数组首元素的指针。如:
                   intfoo(int Array[ ]){ ... }
           该函数声明会被转化成下面的函数声明:
                   intfoo(int * Array){ ... }
   数组作为参数的声明时,上面两种声明方式是等价的,但在其他情况下,不会发生这种自动转化

strncpy与strcpy的区别

两者均用于字符串复制,strncpy是strcpy的安全版本

 1.strcpy的隐患

 strcpy(char *to,const char *from)
该函数判断'\0'作为结束条件,如果目标字符串to的空间不足,则会发生溢出
这是一个潜在的安全隐患,随时有可能会出现错误。

  2.strncopy的安全性
strncpy(char *to,const char *from,int size);

strncpy通过size来控制复制的结束,这个size是源字符串from的大小,这便保证了字符复制的安全性。

这是一种强制性的安全措施,同样它有似乎不可避免的会产生下面的问题:
 1.strncpy不能保证目标字符串to以'\0'结尾。
   这种情况发生在源字符串from长度大于目标字符串to的长度。
 2.源字符串from较小,而目标字符串to较大,将会用大量'\0'填充剩余的空间。


堆与栈的深入理解


使用栈由系统自动分配。如局部变量,系统会自动在栈中为其开辟空间。
             
堆由开发人员申请分配。如在C语言中,开发人员可通过malloc在堆中开辟空间,C++中则使用new。
         
方向:栈:方向由高地址向低地址。
              堆:方向由低地址向高地址。
系统:栈:系统使用一块连续的内存来实现栈。连续的整块内存大小必然有限,故栈一般有大小限制,如果需要的栈空间过大,会出现栈溢出。
             堆:堆空间从自系统空闲内存链表中分配。堆空间可以使不连续的内存,使用操作系统的虚拟内存机制,可获得较大的空间。
 速度:栈:由系统处理,速度较快。
               堆:开发人员通过malloc/new申请使用,速度较慢,并容易产生内存碎片与内存泄露等问题。

程序=数据结构+算法

C基础数据类型
                1.int                  4字节                
                2.char               1字节
                3.float               4字节
                4.double           8字节
                5.bool               1字节

 1.分区
                   代码区
                    静态区
                    堆
                    空闲内存
                    栈

              明确的分区是管理的前提,只有我们确定了房间分那几块区域,才能确定下一步区域摆放哪些物品。确定分区后,我们需要一份清单,记录分类摆放的规则:

           1.代码区:存放程序执行的代码
           2.静态区:全局变量和静态变量(有的同学可能有更深的认识静态区可以 细分为:
                                                     1.非初始化数据段:存放未初始化的全局变量和静态变量。
                                                     2. 初始化的数据:存放初始化的全局变量和静态变量。
           3.堆:动态分配区域,malloc, calloc, realloc等函数
           4.空闲内存:堆栈式可延伸的,空闲内存提供堆向下与栈向上需要的空间
           5.栈:局部变量及每次函数调用时返回地址、以及调用者的环境信息
 C语言设计了这个规则,并严格的为每个程序分配与管理内存,使程序的运行井然有序。一个物品摆放整齐的房间,寻找某件物品会更快,C语言的内存分配机制同样如此,使其拥有更优良的性能。




抱歉!评论已关闭.