4. Hash表的hash函数,冲突解决方法有哪些
Hash表的基本思想:首先在元素的关键字k和元素的存储位置p之间建立一个对应关系f,使得p=f(k),f称为哈希函数。创建哈希表时,把关键字为k的元素直接存入地址为f(k)的单元;以后当查找关键字为k的元素时,再利用哈希函数计算出该元素的存储位置p=f(k),从而达到按关键字直接存取元素的目的。
散列函数:
1) 直接地址
取关键字或关键字的某个线性函数值为哈希地址:H(key)=key 或 H(key)=a·key+b,其中a和b为常数,这种哈希函数叫做自身函数。
2)乘法
该方法包括两个步骤:首先用关键字key乘上某个常数
A(0<A<1)
,并抽取出key.A的小数部分;然后用m乘以该小数后取整。即:
H(key)=m∗⌊key∗A−⌊key∗A⌋⌋
该方法最大的优点是m的选取比除余法要求更低。
3)除法
取关键字被数p除后所得余数为哈希址:
H(key)=keyMODp
,不太常用,因为除法的效率比乘法低,乘数相乘法更合适。
4)分段叠加法
将关键字分割成位数相同的几部分(最后一部分的位数可以不同),然后取这几部分的叠加和(舍去进位)作为哈希地址,这方法称为折叠法(folding)。
5)伪随机
选择一个随机函数,取关键字的随机函数值为它的哈希地址,即
H(key)=random(key)
,其中random为随机函数。通常,当关键字长度不等时采用此法构造哈希函数较恰当。
6)查表法
有一个关键字表,通过关键字可以在该表查询hash值
7)平方取中法
取关键字平方后的中间几位为哈希地址。
解决冲突
1)开放地址法
a)线性探测法
插入元素时,如果发生冲突,算法会简单的从该槽位置向后循环遍历hash表,直到找到表中的下一个空槽,并将该元素放入该槽中,这会使得相同hash值的元素紧挨在一起导致其他hash值的槽被占用。查找元素时,首先散列值所指向的槽,如果没有找到匹配,则继续从该槽遍历hash表,直到:(1)找到相应的元素;(2)找到一个空槽,指示查找的元素不存在,(所以不能随便删除元素);(3)整个hash表遍历完毕(指示该元素不存在并且hash表是满的)
缺点:
① 处理溢出需另编程序。一般可另外设立一个溢出表,专门用来存放上述哈希表中放不下的记录。此溢出表最简单的结构是顺序表,查找方法可用顺序查找。
② 删除工作非常困难。如果将此元素删除,查找的时会发现空槽,则会认为要找的元素不存在。只能标上已被删除的标记,否则,将会影响以后的查找。
③ 容易产生堆聚现象。所谓堆聚现象,就是存入哈希表的记录在表中连成一片。按照线性探测法处理冲突,如果生成哈希地址的连续序列愈长 ( 即不同关键字值的哈希地址相邻在一起愈长 ) ,则当新的记录加入该表时,与这个序列发生冲突的可能性愈大。因此,哈希地址的较长连续序列比较短连续序列生长得快,这就意味着,一旦出现堆聚 ( 伴随着冲突 ) ,就将引起进一步的堆聚。
b)线性补偿探测法
基本思想是:将线性探测的步长从1改为Q,即将上述算法中的
hash=(hash+1)%m
改为:
hash=(hash+Q)%m=hash%m+Q%m
,而且要求 Q 与 m 是互质的,以便能探测到哈希表中的所有单元。
c)伪随机探测
基本思想是:将线性探测的步长从常数改为随机数,即令:
hash=(hash+RN)%m
,其中 RN 是一个随机数。在实际程序中应预先用随机数发生器产生一个随机序列,将此序列作为依次探测的步长。这样就能使不同的关键字具有不同的探测次序,从而可以避 免或减少堆聚。基于与线性探测法相同的理由,在线性补偿探测法和随机探测法中,删除一个记录后也要打上删除标记。
2)拉链法
基本思想:将所有关键字为同义词的结点链接在同一个单链表中。若选定的散列表长度为m,则可将散列表定义为一个由m个头指针组成的指针数 组T[0..m-1]。凡是散列地址为i的结点,均插入到以T[i]为头指针的单链表中。T中各分量的初值均应为空指针。在拉链法中,装填因子α可以大于 1,但一般均取α≤1。
优点:
①拉链法处理冲突简单,且无堆积现象,即非同义词决不会发生冲突,因此平均查找长度较短;
②由于拉链法中各链表上的结点空间是动态申请的,故它更适合于造表前无法确定表长的情况;
③开放定址法为减少冲突,要求装填因子α较小,故当结点规模较大时会浪费很多空间。而拉链法中可取α≥1,且结点较大时,拉链法中增加的指针域可忽略不计,因此节省空间;
④删除结点的操作易于实现。只要简单地删去链表上相应的结点即可。
缺点:
指针需要额外的空间,故当结点规模较小时,开放定址法较为节省空间,而若将节省的指针空间用来扩大散列表的规模,可使装填因子变小,这又减少了开放定址法中的冲突,从而提高平均查找速度。
5. 各种排序:冒泡、选择、插入、希尔、归并、快排、堆排、桶排、基数的原理、平均时间复杂度、最坏时间复杂度、空间复杂度、是否稳定
排序方式 | 最坏时间复杂度 | 空间复杂度 | 稳定性 |
---|---|---|---|
冒泡排序 | O(N^2) | O(1) | 稳定 |
选择排序 | O(N^2) | O(1) | 不稳定 |
插入排序 | O(N^2) | O(1) | 稳定 |
希尔排序 | O(N^2) | O(1) | 不稳定 |
归并排序 | O(N*lgN) | O(N) | 稳定 |
快速排序 | O(N^2) | O(lgN) | 不稳定 |
堆排序 | O(N*lgN) | O(1) | 不稳定 |
桶排序 | O() | O() | O() |
计数排序 | O() | O() | O() |
5.1冒泡法
/**
* 冒泡排序
* 思路:内部循环每走一趟排好一位,依次向后排序
*/
private static void bubbleSort(int[] data) {
int temp;
for (int i = 0; i < data.length; i++) {//每一次确定第i小的数,第i位数据与i之后的数据
for (int j = i+1; j < data.length; j++) {
if (data[i]>data[j]) {
temp =data[i];
data[i]=data[j];
data[j] = temp;
}
}
}
}
5.2 选择法
/**
* 选择排序
* 思路:每次循环得到最小值的下标,然后交换数据。
* 如果交换的位置不等于原来的位置,则不交换。
*/
public static void selectSort(int[] data){
int index=0;
for (int i = 0; i < data.length; i++) {
index = i;//保存最小值下标
for (int j = i; j < data.length; j++) {
if (data[index]>data[j]) {
index = j;
}
}
if (index != i) {
swap(data,index,i);
}
}
}
5.3 插入排序
/**
* 插入排序
* 思路:将数据插入到已排序的数组中。
*/
public static void InsertSort(int[] data) {
int temp;
for (int i = 1; i < data.length; i++) {
temp = data[i];//保存待插入的数值
int j = i;
for (; j>0 && temp<data[j-1]; j--) {
data[j] = data[j-1];
//如果带插入的数值前面的元素比该值大,就向后移动一位
}
//内部循环结束,找到插入的位置赋值即可。
data[j]=temp;
}
}
5.4 希尔排序
参考:白话经典算法系列之三 希尔排序的实现
希尔排序的实质就是分组插入排序,该方法又称缩小增量排序,因DL.Shell于1959年提出而得名。基本思想:先将整个待排元素序列分割成若干个子序列(由相隔某个“增量”的元素组成的)分别进行直接插入排序,然后依次缩减增量再进行排序,待整个序列中的元素基本有序(增量足够小)时,再对全体元素进行一次直接插入排序。因为直接插入排序在元素基本有序的情况下(接近最好情况),效率是很高的。
/**
* 希尔排序(缩减增量排序)
* 想想也不难。
* 思路:三层循环
* 第一层循环:控制增量-增量随着程序的进行依次递减一半
* 第二层循环:遍历数组
* 第三层循环:比较元素,交换元素。
* 这里需要注意的是:比较的两个元素和交换的两个元素是不同的。
*/
public static void shellSort(int[] data) {
int k;
for (int div = data.length/2; div>0; div/=2) {
for (int j = div; j < data.length; j++) {
int temp = data[j];
for (k=j; k>=div && temp<data[k-div] ; k-=div) {
data[k] = data[k-div];
}
data[k] = temp;
}
}
}
5.5 归并排序
基本思路:将两个已经排好序的数组插入到第三个数组当中。分割并排序
/**
*data 待排序数组
*temp 存储排序数组
*left 左边数组起始位置
*right 右边数组起始位置
*center 左右数组断点位置
*/
public static void mergeSort(int[] data,int[] temp,int left,int center,int right){
int leftEnd = center;
int rightStar = center+1;
int len = right-left+1;
int tempPos = left;//排序数组下标
//将两个已经排序的数组进行比较,将元素添加到temp数组中保存。
while (left<=leftEnd&&rightStar<=right) {
if (data[left]<=data[rightStar]) {
temp[tempPos++] = data[left++];
}else {
temp[tempPos++] = data[rightStar++];
}
}
//右数组空,左数组未空
while (left<=leftEnd) {
temp[tempPos++]=data[left++];
}
//左数组空,右数组未空
while (rightStar<=right) {
temp[tempPos++]=data[rightStar++];
}
//将排序结果拷贝回原来的数组
for (int i = 0; i < len; i++,right--) {
data[right]=temp[right];
}
}
5.6 快速排序
基本思想是:1)先从数列中取出一个数作为基准数;2)分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边;3)再对左右区间重复第二步,直到各区间只有一个数。
参考:白话经典算法系列之六 快速排序 快速搞定