数据结构面试整理(2)

来源:互联网 发布:仓库管理源码php 编辑:程序博客网 时间:2024/05/21 17:18

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)=mkeyAkeyA
该方法最大的优点是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(hash1)%m改为:hash(hashQ)%m=hash%m+Q%m,而且要求 Q 与 m 是互质的,以便能探测到哈希表中的所有单元。
c)伪随机探测
基本思想是:将线性探测的步长从常数改为随机数,即令: hash(hashRN)%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)再对左右区间重复第二步,直到各区间只有一个数。
参考:白话经典算法系列之六 快速排序 快速搞定

5.7 堆排序

白话经典算法系列之七 堆与堆排序

5.8 桶排序

原创粉丝点击