数据结构和算法——哈希表

243 阅读5分钟

一、哈希表简介

场景

你是一个超市售货员,超市中放了超级多的物品,为了收银时能很快完成任务,就需要快速知道每种商品的具体价格,可以采用什么策略呢?

我们当然可以把这些种类的物品都写入一个本本,并且按照名称排序。这样查找某一个商品价格时,就可以按照二分法查找了,这个时候的时间复杂度是O(log n)。

如果我们去最强大脑节目中请一个大佬,他可以轻轻松松的记住所有的商品价格,当我们需要查找某一个商品,直接问他,他就能告诉我们价格,这个时候的时间复杂度是O(1)

这是不是很快呀?而在数据结构中,这个记忆力超级好的大牛就是哈希表

简介

哈希表(Hash table,也叫散列表),是根据关键码值(Key value)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列函数,存放记录的数组叫做散列表。

通俗一点说,我们创建一个数组,通过某种映射关系把这个对象key(比如商品),映射到数组所在的位置(索引),然后数组中存的就是这个值value(比如价格),然后想查找价格时就可以通过这个对象key映射到数组相应的位置,就可以很快定位到value了。

二、哈希函数

我们对象和索引位置的映射关系是靠哈希函数来确定的,可以通过这样几种方式确定哈希函数。

  • 直接定址法:取关键字或关键字的某个线性函数值为散列地址。这是一种线性的方式,比如1990年出生的放入索引0的位置,1991年的放入索引1的位置,依此类推...
  • 数字分析法:直接定址法可能会造成哈希冲突比较多,这个时候可以采用数字分析法,去找变化比较大的方式。还比如按照年龄记录,如果是仅仅按照年份来映射位置,那么所有1990年出生的都会放在同一个索引位置,这样就不能很好的查找。这时可以采用月+日的函数来定位索引,比如生日5月10日的可以放入0510的位置,生日1月4日的可以放入0104位置,这样冲突的可能性更小;
  • 平方取中法:比如几个数是【101、233、144】,通过平方可以得到【10201、54289、20736】,可以通过取中法得到【20、28、73】,放入相应的位置;
  • 除留取余法:可以通过除一个数,得到余数来获取索引位置;
  • 随机数法:对于那些长度不相同的对象,不方便直接求得索引,可以使用Random()方法获取索引。

三、哈希冲突的处理策略

哈希冲突

虽然哈希表很好,我们可以建立很快捷查找的方式,可是还是有一些问题。有的时候我们的散列函数有可能把两个对象都映射到一个数组的key中,造成哈希冲突。比如:

String str1 = "通话";
String str2 = "重地";
System.out.println(str1.hashCode());//1179395
System.out.println(str2.hashCode());//1179395

这种情况还是很容易出现的毕竟我们的数组位置有限,而对象是可以不断创建的,那么如何解决哈希冲突的问题呢?

解决办法

  • 开放地址法
    • 线性探测法:也就是发生哈希冲突时,直接存入它的下一个位置,看看是否冲突,冲突再往下;
    • 二次探测法:从发生冲突的单元加上1^2,2^2,3^2,...,n^2,直到遇到空闲的单元;
    • 再哈希法:同时构造多个不同的哈希函数,Hi = RHi(key) i= 1,2,3 … k; 当H1 = RH1(key) 发生冲突时,再用H2 = RH2(key) 进行计算,直到冲突不再产生,这种方法不易产生聚集,但是增加了计算时间;
  • 链地址法:数组下面加链表存储,比较常见的就是Java的HashMap。
img

四、HashMap的代码实现

public class HashTabDemo {
    public static void main(String[] args) {
        //创建哈希表
        HashTab hashTab = new HashTab(7);

        //写一个简单的菜单
        String key = "";
        Scanner scanner = new Scanner(System.in);
        while(true) {
            System.out.println("add:  添加雇员");
            System.out.println("list: 显示雇员");
            System.out.println("find: 查找雇员");
            System.out.println("exit: 退出系统");

            key = scanner.next();
            switch (key) {
                case "add":
                    System.out.println("输入id");
                    int id = scanner.nextInt();
                    System.out.println("输入名字");
                    String name = scanner.next();
                    //创建 雇员
                    Emp emp = new Emp(id, name);
                    hashTab.add(emp);
                    break;
                case "list":
                    hashTab.list();
                    break;
                case "find":
                    System.out.println("请输入要查找的id");
                    id = scanner.nextInt();
                    hashTab.findEmpById(id);
                    break;
                case "exit":
                    scanner.close();
                    System.exit(0);
                default:
                    break;
            }
        }
    }
}

//雇员类
class Emp{
    public int id;
    public String name;
    public Emp next;

    //初始化的next始终为空
    public Emp(int id, String name) {
        super();
        this.id = id;
        this.name = name;
    }
}

//创建List集合
class EmpLinkedList{
    //头指针
    private Emp head;

    //添加员工到list集合中
    public void add(Emp emp){
        if (head == null){
            head = emp;
            return;
        }
        //创建一个临时元素用于遍历
        Emp curEmp = head;
        while (true){
            if (curEmp.next == null){
                break;
            }
            curEmp = curEmp.next;
        }
        curEmp.next = emp;
    }

    //遍历链表的雇员信息
    public void list(int no){
        if (head == null){
            System.out.println("第 "+(no+1)+" 链表为空");
            return;
        }
        System.out.println("--------链表的信息-------");
        Emp curEmp = head;
        while (true){
            System.out.printf("id = %d name = %s",curEmp.id,curEmp.name);
            if (curEmp.next == null){
                break;
            }
            curEmp = curEmp.next;
        }
        System.out.println();
    }

    //根据id查找雇员
    //如果查找到,就返回Emp, 如果没有找到,就返回null
    public Emp searchEmpById(int no){
        if (head == null){
            throw new RuntimeException("当前链表没有元素,无法查看");
        }
        Emp curEmp = head;
        while (true){
            if (curEmp.id == no){
                break;
            }
            if (curEmp.next == null){
                curEmp = null;
            }
            curEmp = curEmp.next;
        }
        return curEmp;
    }
}

//创建hashtab,用来作为哈希表
class HashTab{
    private EmpLinkedList[] empLinkedListArr;
    private int size;//表示有多少条链表

    //构造器
    public HashTab(int size){
        this.size = size;
        //初始化EmpLinkedList数组
        empLinkedListArr = new EmpLinkedList[size];
        for (int i = 0; i < empLinkedListArr.length; i++) {
            empLinkedListArr[i] = new EmpLinkedList();
        }
    }

    //编写哈希函数,以便于映射数组
    public int hashFunc(int id){
        //使用id与总长度取模求得
        return id % size;
    }

    //添加雇员
    public void add(Emp emp){
        //求取当前元素的哈希值
        int hashNum = hashFunc(emp.id);
        empLinkedListArr[hashNum].add(emp);
    }

    //遍历所有的链表
    public void list(){
        for (int i = 0;i < empLinkedListArr.length;i++){
            empLinkedListArr[i].list(i);
        }
    }

    //根据输入的id,查找雇员
    public void findEmpById(int id){
        int hashNum = hashFunc(id);
        Emp emp = empLinkedListArr[hashNum].searchEmpById(id);
        if (emp != null){
            System.out.printf("在第%d条链表中找到 雇员 id = %d\t", (hashNum + 1), id);
        }else{
            System.out.println("在哈希表中,没有找到该雇员~");
        }
    }
}