数据结构 之 哈希表

1.什么是哈希表?

  哈希表是一种数据结构,它可以提供快速的插入和删除操作。如果存储在哈希表中的数据较好的满足了哈希表的要求,那么在哈希表中执行插入和操作只需要接近常量的时间,即时间复杂度为o(1),但哈希表也不是十全十美的,它也存在着缺点,这都会在下面慢慢谈到.

2.哈希表的存储方式

哈希表是通过数组来存储数据的,但数据并不是直接放入数组中(直接放入就是数组存储啦!)。说到这里就需要谈到哈希化,而哈希表的核心部分,我认为就是哈希化了:简而言之,将数据插入哈希表中的数组的相应位置的规则,就是哈希化,再简单来说,就是将数据通过某种方法(即哈希函数,可以自己定义)转化得到一个数据下标(这就是这个数据应该存储的位置),将该数据放入到该下标对应的数组位置中,使用哈希函数向数组中插入数据,得到的就是一个哈希表。这里来用一个简单的示例来向大家介绍哈希表:

如果我们需要存储5个数据:5 、 31、 58 、75 、87  存储在容量为10的哈希表中。

1.首先,我们需要定义一个哈希函数,我这里的定义的哈希函数为 arrayIndex = Number % arraySize;这里arrayIndex就对应着要插入的数组坐标位置;

2.计算数组下标,上面的数据计算结果为:5, 1, 8,5,7

3.向数组中插入数据,得到哈希表


0


1


2


3


4


5


6


7


8


9


31


5


75


87


58

这样子来看,是不是简单的!但事实上对于这几个数据来说确实挺简单的。但是这里我们还有很多东西没有去讨论,大家继续向下看:

大家注意到没有,5和75得到的数组下标都是5,它们产生了冲突,那它怎么插入到数组中的呢?对于这种情况有很多中方式处理,这里用的是开放地址法的线性探测方法。

这里就介绍这几中解决这种冲突的方法

3.冲突解决

这里解决的方法主要有两种:开放地址法和链地址法

  一、开放地址法

    开放地址法包括三种方法:线性探测(上面用到的)、二次探测、再哈希法。

    1.线性探测

    在线性探测的方法中,当冲突产生时会线性的查找空白单元。正如上面所提到的,当插入75时,因为当前数组下标为5的位置被数据 5 所占据,因此数组下标就会继续向下查找,此时下标为5的数组位置是空闲的,数据75就插入其中;如果此时我们再插入一个元素45,开始查询数组下标位置为5的数组位置,不为空---》查询数组下标为6:不为空---》查询数组下标为7:不为空---》查询数组下标为8:不为空---》查询数组下标为9:为空---》在数组下标为9的位置插入45.


0


1


2


3


4


5


6


7


8


9


31


5


75


87


58


45

    哈希表中,一连串的已填充单元叫做填充序列,增加越来越多数据项,填充序列就越来越长,这叫做聚集。例如上图中的位置5-9中数据。这个时候就需要思考一个问题,如果数据大量聚集的话,会怎么样呢?对于线性探测来说,查找和插入的一个数据项就会花费更多的时间,事实上对于一个存储大量数据的哈希表来说,此时的效率非常低。    

     2.二次探测

     在线性探测的方法中,易发生聚集。一旦聚集形成,它就会越来越大。那些后来哈希化后的落在聚集范围周围的数据,都要一步步移动,并且插入到聚集的最后,因此聚集再一次变大。聚集越大,会有更多的数据落在这个范围内,就这样会形成恶性循环,极大地影响哈希表的效率。

     已填入哈希表中的数据项和表长的比率称为装填因子。有10000个数据,如果填入6667个数据,那么他的装填因子就是2/3。当装填因子不太大时,聚集就会比较连贯。哈希表的某个部分可能包含大量的聚集,而另一部分可能还很稀疏。

      二次探测是防止聚集的一种尝试(并不能解决全部的聚集问题),思想不再是顺序探测下一个位置,而是间隔的探测下一个位置。如果原始下标是x,那么在线性探测中,探测的位置为x,x+1,x+2。。。;而在二次探测中探测的则是x+1,x+4,x+9。。。

     二次探测消除了在线性探测中产生的聚集问题,这种聚集问题叫做原始聚集。可是我们想一想插入一些特殊的数据时:5,15,25,35。。。之类的数据,就会发现在探测空位置时,它们的探测的顺序总是一样的,这种情况也是一种聚集,只不过相比于线性探测,它更加不容易产生,这种现象叫做二次聚集

    3.再哈希法

    为了消除原始聚集和二次聚集,可以使用另一个方法:再哈希法。

    在再哈希法中,是把第一次产生的由哈希函数产生的哈希值(第一个数组下标),使用另一个哈希函数再次哈希化,得到的另一个新的值,来作为探测的步长。第二个哈希函数必须具备两个特点:1.和第一个哈希函数不同。2.不能输出为零(否则将没有步长,每次探测都会是相同位置)。专家们发现下面形式的哈希函数工作的非常好:stepSize = constant * (key % constant);其中constant是质数,且小于数组容量。例如:stepSize = 5 * (key % 5)。示例:

    对于上面的例子:当我们插入5、75、45 时,5插入到下标为5的位置中(这里的二次哈希的函数用的是上面的示例),但是75用再哈希法得到的探测步长却为0,45得到的探测步长为也为0,这无疑不是一个好消息。此时就牵扯到另一个问题:数组的容量选取。对于哈希表中的容量,应该总是选取一个质数,例如上面的数组长度应该选取11来代替10(后面的例子就用数组长度11)。如果数组容量不是质数,就容易出现上面的探测步长为0情况,那么探测步长就会变得无限长。

    如果此时用数组容量11来代替数组容量10 。插入5、75、45时,75得到的探测步长是20,45得到的探测步长为5。(如果只使用再哈希化法,在constant选取5时,对于除了第一次哈希为5的,剩下哈希化为5的插入就会报错)。

  二、链地址法

  在开放地址法中,通过在哈希表中寻找到一个空位来解决冲突问题。另一个方法是在哈希表中的每个单元中设置链表,某个数据项的关键字值还是像往常一样映射到哈希表的单元中,而数据项本身则插入到链表中。其他的产生冲突的数据项只需要加到链表中就可以了,不再去寻找空位。

三、代码实现哈希表(java实现)

开放地址法:

定义的数据类:

/*
 * 存储的数据类
 */
public class DataItem {
    private int iData;
    public DataItem(int i){
        iData = i;
    }
    public int getkey(){
        return iData;
    }
}

线性探测和二次探测的只有一点点区别、这里贴出线性探测的代码:

package hash;
/*
 *******************顺序哈希算法 *******************
 */
public class HashTable {
    private DataItem[] hashArray;
    private int arraySize;
    private DataItem nonItem;
    //结构体
    public HashTable(int size){
        arraySize = size;
        hashArray = new DataItem[arraySize];
        nonItem = new DataItem(-1);
    }
    //显示哈希表
    public void displayTable(){
        System.out.print("Table: ");
        for (int j = 0; j < arraySize; j++) {
            if (hashArray[j]!=null) {
                System.out.print(hashArray[j].getkey()+" ");
            }else {
                System.out.print("** ");
            }
        }
        System.out.println("");
    }
    //hashFunc
    public int hashFunc(int key){
        return key%arraySize;
    }
    //insert
    public void  insert(DataItem item) {
        int key = item.getkey();
        int hashVal = hashFunc(key);
        while(hashArray[hashVal]!=null&&hashArray[hashVal].getkey()!=-1){
            ++hashVal;
            hashVal %= arraySize;
        }
        hashArray[hashVal] = item;
    }
    //delete
    public DataItem delete(int key){
        int hashVal = hashFunc(key);
        while(hashArray[hashVal]!=null){
            if (hashArray[hashVal].getkey()==key) {
                DataItem temp = hashArray[hashVal];
                hashArray[hashVal] = nonItem;
                return temp;
            }
            ++hashVal;
            hashVal %= arraySize;
        }
        return null;
    }
    //find
    public DataItem find(int key){
        int hashVal = hashFunc(key);
        while(hashArray[hashVal]!=null){
            if (hashArray[hashVal].getkey()==key) {
                DataItem temp = hashArray[hashVal];
                return temp;
            }
            ++hashVal;
            hashVal %= arraySize;
        }
        return null;
    }
}

再哈希法:

package hashDouble;
/*
 * 哈希表的使用:再哈希存储
 */
public class HashTable {
    private DataItem[] hashArray;
    private int arraySize;
    private DataItem nonItem;
    //contruct
    public HashTable(int size){
        arraySize = size;
        hashArray = new DataItem[arraySize];
        nonItem = new DataItem(-1);
    }
    //displayTable
    public void displayTable(){
        System.out.print("Table: ");
        for (int i = 0; i < arraySize; i++) {
            if (hashArray[i]!=null) {
                System.out.print(hashArray[i].getKey() + " ");
            }else {
                System.out.print("** ");
            }
        }
    }
    //hashFuc
    public int hashFuc(int key){
        return key%arraySize; //arraySize为哈希表的长度
    }
    public int hashFuc2(int key){
        return 5-key%5;
    }
    //insert
    public void insert(int key, DataItem item){
        int hashVal = hashFuc(key);
        int stepSize = hashFuc2(key);
        while (hashArray[hashVal]!=null&&hashArray[hashVal].getKey()!=-1) {
            hashVal += stepSize;
            hashVal %=arraySize;
        }
        hashArray[hashVal]=item;
    }
    //delete
    public DataItem delete(int key){
        int hashVal = hashFuc(key);
        int stepSize = hashFuc2(key);
        while (hashArray[hashVal]!=null) {
            if (hashArray[hashVal].getKey()==key) {
                DataItem temp = hashArray[hashVal];
                hashArray[hashVal]=nonItem;
                return temp;
            }
            hashVal += stepSize;
            hashVal %= arraySize;
        }
        return null;
    }
    //find
    public DataItem find(int key){
        int hashVal = hashFuc(key);
        int stepSize = hashFuc2(key);
        while (hashArray[hashVal]!=null) {
            if (hashArray[hashVal].getKey()==key) {
                return hashArray[hashVal];
            }
            hashVal += stepSize;
            hashVal %= arraySize;
        }
        return null;
    }
}

操作Mian主类(看下方)

链地址法较为复杂,希望大家能好好看看:

数据类:

package linkHash;
/*
 * 定义的链表中存储的数据
 */
public class Link {
    private int iData;
    public Link next;
    public Link(int item){
        iData = item;
    }
    public int getKey(){
        return iData;
    }
    public void disPlayItem(){
        System.out.print(iData + " ");
    }
}

链表的定义:

package linkHash;
/*
 * 排序链表的实现
 */
public class SortedList {
    private Link first;
    //构造体
    public SortedList(){
        first = null;
    }
    //插入链表
    public void insert(Link theLink){
        int key = theLink.getKey();
        Link previous = null;
        Link current = first;
        while (current != null && key > current.getKey()) {
            previous = current;
            current = current.next;
        }
        if (previous == null) {
            first = theLink;
        }else{
            previous.next = theLink;
        }
        theLink.next = current;
    }
    //删除链表
    public void delete(int key) {
        Link previous = null;
        Link current = first;
        while(current!=null && key != current.getKey()){
            previous = current;
            current = current.next;
        }
        if (previous == null) {
            first = first.next;
        }else {
            previous.next = current.next;
        }
    }
    //find
    public Link find(int key){
        Link current = first;
        while (current != null && key >= current.getKey()) {
            if (current.getKey()==key) {
                return current;
            }
            current = current.next;
        }
        return null;
    }
    //displayList
    public void displayList(){
        Link current = first;
        System.out.print("List(first--->last):");
        while (current != null) {
            current.disPlayItem();
            current = current.next;
        }
        System.out.println("");
    }
}

哈希表的定义:

package linkHash;

public class HashTable {
    private SortedList[] hashList;
    private int arraySize;
    //HashTable()
    public HashTable(int size){
        arraySize = size;
        hashList = new SortedList[arraySize];
        for (int j = 0; j < arraySize; j++) {
            hashList[j] = new SortedList();
        }
    }
    //hashFunc
    public int hashFunc(int key) {
        return key%arraySize;
    }
    //insert
    public void insert(Link theLink){
        int key = theLink.getKey();
        int hashVal = hashFunc(key);
        hashList[hashVal].insert(theLink);
    }
    //delete
    public void delete(int key){
        int hashVal = hashFunc(key);
        hashList[hashVal].delete(key);
    }
    //find
    public Link find(int aKey){

        int hashVal = hashFunc(aKey);
        Link theLink = hashList[hashVal].find(aKey);
        return theLink;
    }
    //displayTable
    public void displayTable(){
        for (int j = 0; j < arraySize; j++) {
            System.out.print(j + ".");
            hashList[j].displayList();
        }
    }
}

操作主类:

package linkHash;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;

public class linkHashApp {
    public static void  main(String[] args) throws IOException {
        int aKey;
        Link aDataItem;
        int size,n,keysPerCell = 100;
        System.out.print("Enter size of hash table:");
        size = getInt();
        System.out.print("Enter initial number of items:");
        n = getInt();
        HashTable theHashTable = new HashTable(size);
        for (int j = 0; j < n; j++) {
            aKey = (int)(java.lang.Math.random() * keysPerCell * size);
            aDataItem = new Link(aKey);
            theHashTable.insert(aDataItem);
        }
        while (true) {
            System.out.print("Enter first letter of show, insert, delete, find:");
            char choice = getChar();
            switch (choice) {
            case ‘s‘:
                theHashTable.displayTable();
                break;
            case ‘i‘:
                System.out.print("Enter a key to insert:");
                aKey = getInt();
                aDataItem = new Link(aKey);
                theHashTable.insert(aDataItem);
                break;
            case ‘d‘:
                System.out.print("Enter a key to delete:");
                aKey = getInt();
                theHashTable.delete(aKey);
                break;
            case ‘f‘:
                System.out.println("Enter a key to find:");
                aKey = getInt();
                aDataItem = theHashTable.find(aKey);
                if (aDataItem != null) {
                    System.out.println("Found " + aKey);
                }else{
                    System.out.println("Can‘t found " + aKey);
                }
                break;

            default:
                break;
            }
        }
    }
    public static String getString() throws IOException{
        InputStreamReader in = new InputStreamReader(System.in);
        BufferedReader buff = new BufferedReader(in);
        String s = buff.readLine();
        return s;
    }
    public static int getInt() throws  IOException {
        return Integer.parseInt(getString());
    }
    public static char getChar() throws IOException{
        return getString().charAt(0);
    }
}

---------------------------------------------------------------------------------------------------------------

总结所得,里面有什么谬误的地方,欢迎大家指正。如果有什么建议和看法,欢迎大家留言。

时间: 2024-11-03 21:03:02

数据结构 之 哈希表的相关文章

【算法与数据结构】哈希表-链地址法

哈希表的链地址法来解决冲突问题 将所有关键字为同义词的记录存储在同一个线性链表中,假设某哈希函数产生的哈希地址在区间[0, m - 1]上,则设立一个至振兴向量 Chain  ChainHash[m]; 数据结构 //链表结点 typedef struct _tagNode { int data; //元素值(关键字) struct _tagNode* next; //下一个结点 }Node, *PNode; //哈希表结点 typedef struct _tagHashTable { //这里

数据结构之哈希表--预习篇

数据结构实验:哈希表 Time Limit: 1000ms   Memory limit: 65536K  有疑问?点这里^_^ 题目描述 在n个数中,找出出现次数最多那个数字,并且输出出现的次数.如果有多个结果,输出数字最小的那一个. 输入 单组数据,第一行数字n(1<=n<=100000). 接下来有n个数字,每个数字不超过100000000 输出 出现次数最多的数字和次数. 示例输入 3 1 1 2 示例输出 1 2 首先声明本渣还没学会哈希,这篇就当哈希的预习篇吧 写的不好大家见谅 首

浅谈算法和数据结构: 十一 哈希表

在前面的系列文章中,依次介绍了基于无序列表的顺序查找,基于有序数组的二分查找,平衡查找树,以及红黑树,下图是他们在平均以及最差情况下的时间复杂度: 可以看到在时间复杂度上,红黑树在平均情况下插入,查找以及删除上都达到了lgN的时间复杂度. 那么有没有查找效率更高的数据结构呢,答案就是本文接下来要介绍了散列表,也叫哈希表(Hash Table) 什么是哈希表 哈希表就是一种以 键-值(key-indexed) 存储数据的结构,我们只要输入待查找的值即key,即可查找到其对应的值. 哈希的思路很简单

浅谈数据结构:哈希表

一.  基本概念 哈希表(hash table )是一种根据关键字直接访问内存存储位置的数据结构,通过哈希表,数据元素的存放位置和数据元素的关键字之间建立起某种对应关系,建立这种对应关系的函数称为哈希函数 二.哈希表的构造方法 假设要存储的数据元素个数是n,设置一个长度为m(m > n)的连续存储单元,分别以每个数据元素的关键字Ki(0<=i<=n-1)为自变量,通过哈希函数hash(Ki),把Ki映射为内存单元的某个地址hash(Ki),并将数据元素存储在内存单元中 从数学的角度看,哈

【经典数据结构】哈希表

哈希表的基本概念 哈希表,也叫散列表,它是基于快速存取的角度设计的,是一种典型的“空间换时间”的做法.哈希表是普通数组的一种推广,因为数组可以直接寻址,故可在O(1)时间内访问数组的任意元素. 哈希表是根据关键字(Key Value)而直接进行访问的数据结构.也就是说,它将关键字通过某种规则映射到数组中的某个位置,以加快查找的速度.这个映射规则称为哈希函数(散列函数),存放记录的数组称为哈希表.哈希表建立了关键字和存储地址之间的一种直接映射关系. 若多个不同的关键字通过哈希函数计算得到相同的数组

数据结构是哈希表(hashTable)

哈希表也称为散列表,是根据关键字值(key value)而直接进行访问的数据结构.也就是说,它通过把关键字值映射到一个位置来访问记录,以加快查找的速度.这个映射函数称为哈希函数(也称为散列函数),映射过程称为哈希化,存放记录的数组叫做散列表.比如我们可以用下面的方法将关键字映射成数组的下标:arrayIndex = hugeNumber % arraySize. 哈希化之后难免会产生一个问题,那就是对不同的关键字,可能得到同一个散列地址,即同一个数组下标,这种现象称为冲突,那么我们该如何去处理冲

程序员,你应该知道的数据结构之哈希表

哈希表简介 哈希表也叫散列表,哈希表是一种数据结构,它提供了快速的插入操作和查找操作,无论哈希表总中有多少条数据,插入和查找的时间复杂度都是为O(1),因为哈希表的查找速度非常快,所以在很多程序中都有使用哈希表,例如拼音检查器. 哈希表也有自己的缺点,哈希表是基于数组的,我们知道数组创建后扩容成本比较高,所以当哈希表被填满时,性能下降的比较严重. 哈希表采用的是一种转换思想,其中一个中要的概念是如何将键或者关键字转换成数组下标?在哈希表中,这个过程有哈希函数来完成,但是并不是每个键或者关键字都需

数据结构【哈希表】

哈希表(Hash Table)基本概念 哈希表(Hash Table)是一种根据关键字(Key value)直接访问内存存储位置的数据结构.通过哈希表,数据元素的存放位置和数据元素的关键字之间建立起某种映射对应关系,这个映射函数叫做散列函数,存放数据的数组叫做散列表. 哈希函数构造方法 哈希表的构造方法是: 假设要存储的数据元素个数为n,设置一个长度为m(m≥n)的连续存储单元,分别以每个数据元素的关键字 Ki(0<= i <=n-1) 为自变量,通过哈希函数 hash(Ki) 把 Ki 映射

【数据结构】哈希表的线性探测算法

构造哈希表常用的方法是: 除留余数法--取关键值被某个不大于散列表长m的数p除后的所得的余数为散列地址.HashKey= Key % P. 直接定址法--取关键字的某个线性函数为散列地址HashKey= Key 或 HashKey= A*Key + BA.B为常数. 我在这里主要使用一下除留余数法Hash(key) =Key%P,(P这里是哈希表的长度)p最好是素数考虑降低哈希冲突的原因,我并没有在这上面过于追究此处哈希表长度10,见线性探测图. 哈希表经常遇到的一个问题就是哈希冲突. 哈希冲突