一文学懂哈希及如何手撕哈希算法(附详细例题)

一、概念理解

哈希(Hashing)是计算机科学中一个非常重要的概念,广泛应用于数据存储、数据检索、加密、校验等场景。哈希的核心思想是将输入的数据(通常是任意长度)通过哈希函数映射为固定长度的输出,这个输出值称为哈希值或哈希码。

1. 哈希的基本概念

哈希是将任意长度的数据(例如文本、数字、文件等)通过某种算法转换成一个固定长度的值。哈希值通常是一个数字或字符序列,其长度固定。

  • 哈希函数:哈希函数是将输入数据(称为“消息”或“键”)映射到一个固定大小的输出(哈希值)的数学函数。

    对于任何输入值 xx,哈希函数 H(x)H(x) 会输出一个固定长度的值。常见的哈希函数包括 MD5、SHA-1、SHA-256 等。

  • 哈希值(哈希码):是哈希函数的输出。它通常被用来在查找、验证、加密等过程中进行快速比较。

2. 哈希函数的性质

一个好的哈希函数需要满足以下几个基本性质:

  • 确定性:对于相同的输入,哈希函数的输出应该是相同的。

  • 快速计算:哈希函数应该能够快速地计算出哈希值,无论输入数据的大小如何。

  • 均匀分布:理想情况下,哈希函数应该尽量将不同的输入数据映射到哈希值的不同位置,从而避免哈希冲突。也就是说,哈希值的分布应该尽量均匀。

  • 抗碰撞性(Collision Resistance):不同的输入数据应该尽量产生不同的哈希值。如果不同输入数据产生相同的哈希值,称为哈希冲突(Collision)。好的哈希函数应该使得碰撞的概率非常低。

  • 不可逆性:哈希函数应当是单向的,即从哈希值无法反推出原始数据。这样的性质是加密学中不可或缺的一部分。

  • 抗预映像攻击:给定一个哈希值,应该很难找到一个输入数据,使得哈希值为该哈希值。

3. 常见的哈希算法

哈希算法在不同的领域有不同的应用。常见的哈希算法包括:

  • MD5(Message Digest Algorithm 5)

    • 输出:128位(16字节)的哈希值,通常以32个字符的十六进制表示。
    • 应用:早期广泛用于文件校验、密码存储等,但由于其存在较多碰撞问题,现在已不推荐用于安全敏感的应用。
  • SHA-1(Secure Hash Algorithm 1)

    • 输出:160位(20字节)的哈希值。
    • 应用:曾广泛用于数字签名、证书验证等,但现已被认为不再足够安全,已被逐渐淘汰。
  • SHA-256

    • 输出:256位(32字节)的哈希值。
    • 应用:SHA-2系列(包括SHA-256)目前被认为是相对安全的哈希算法,广泛应用于加密货币(如比特币)、数字签名、文件校验等领域。
  • CRC32

    • 输出:32位的哈希值,通常用于检查数据的完整性。
    • 应用:用于文件传输、存储系统中,检测数据是否被篡改。
  • BLAKE2

    • 输出:可定制长度的哈希值,性能优秀,安全性较高。
    • 应用:比SHA-2更加高效,广泛用于密码学、数字签名等。

4. 哈希的应用

哈希有广泛的应用,以下是一些主要的应用场景:

4.1 数据结构:哈希表

哈希表(Hash Table)是一种基于哈希算法的常用数据结构。它通过哈希函数将键映射到数组的索引位置,从而提供快速的查找、插入和删除操作。哈希表广泛应用于数据库索引、缓存系统等。

  • 哈希冲突(Collision):当不同的输入数据经过哈希函数映射到相同的位置时,就发生了哈希冲突。常用的解决哈希冲突的方法包括:
    • 链式法(Chaining):每个哈希桶使用链表存储多个元素。若哈希值相同,则将元素插入链表中。
    • 开放寻址法(Open Addressing):当发生哈希冲突时,寻找数组中的下一个空位来存储元素,直到找到为止。
4.2 数据完整性校验

哈希常用于验证数据的完整性。例如,在文件传输过程中,源和目标可以使用相同的哈希函数生成文件的哈希值,然后比较两个哈希值,若相同,则说明文件未被篡改。

  • 校验和(Checksum):数据传输协议中,常常会用哈希值来校验数据的完整性。比如,FTP、HTTP传输文件时,通常会附带文件的哈希值(如MD5或SHA-1)。
4.3 加密与数字签名

哈希算法在加密学中用于生成消息摘要,并结合公钥或私钥进行数字签名。数字签名可以验证消息的完整性和发送者身份。

  • 数字签名:发送方用私钥对消息的哈希值进行加密,接收方通过公钥解密并与接收到的消息计算哈希值比对,从而验证消息是否被篡改。
4.4 密码存储

在密码存储中,直接存储用户密码是不安全的,因此通常存储的是密码的哈希值。这样,即便数据库被攻击者获取,密码本身仍然保持安全。

  • 盐(Salt):为了防止通过预计算哈希值表(如彩虹表)进行攻击,通常会在密码的哈希计算中加入一个随机生成的值(盐)。每个用户的盐值不同,从而保证即使两个用户的密码相同,哈希值也不同。
4.5 区块链

在区块链中,哈希函数被用于确保数据不可篡改。每个区块的哈希值包含前一个区块的哈希值,形成一个链条,任何篡改都会导致整个链的哈希值变化,从而被网络中的节点察觉。

4.6 数据去重与唯一性检查

哈希函数可以用于去重和唯一性检查。例如,在大规模数据处理、数据库去重等场景中,可以通过计算数据的哈希值来判断数据是否重复。

5. 常见的哈希相关问题

  • 哈希冲突:尽管哈希函数可以将不同的输入映射到固定大小的输出,但由于输出的空间有限,不同的输入可能会生成相同的哈希值(碰撞)。这对于哈希表、加密等应用来说是一个严重问题。解决哈希冲突的方法包括使用更复杂的哈希算法、增加哈希表的大小或采用链式存储等。

  • 哈希碰撞攻击:攻击者可能通过巧妙的输入设计,使得两个不同的输入数据产生相同的哈希值,从而进行欺骗或篡改。这在使用不安全的哈希函数(如MD5、SHA-1)时较为容易发生。

6. 总结

哈希是一种非常重要的技术,广泛应用于计算机科学中的数据存储、加密、安全验证等领域。一个好的哈希函数需要具备快速计算、抗碰撞、不可逆等特性,而哈希算法的选择则依赖于具体的应用场景。在实际应用中,了解哈希的工作原理,掌握常见哈希算法的特性,能够帮助你在多个技术领域中更好地使用哈希。

二、手撕算法题

当然!以下是用 Java 编写的相应解答,展示了哈希在各种常见算法问题中的应用。

1. 查找和匹配

两数之和

给定一个整数数组和一个目标值,找出数组中两个数之和等于目标值的索引。我们可以使用哈希表来加速查找过程,避免使用暴力算法。

import java.util.HashMap;

public class TwoSum {
    public static int[] twoSum(int[] nums, int target) {
        HashMap<Integer, Integer> map = new HashMap<>();
        for (int i = 0; i < nums.length; i++) {
            int complement = target - nums[i];
            if (map.containsKey(complement)) {
                return new int[] { map.get(complement), i };
            }
            map.put(nums[i], i);
        }
        throw new IllegalArgumentException("No solution");
    }

    public static void main(String[] args) {
        int[] nums = {2, 7, 11, 15};
        int target = 9;
        int[] result = twoSum(nums, target);
        System.out.println("Indices: " + result[0] + ", " + result[1]);
    }
}

2. 去重

去重数组中的元素

使用哈希集合(HashSet)来去除重复的元素,可以在 O(n) 时间内完成。

import java.util.HashSet;

public class RemoveDuplicates {
    public static int removeDuplicates(int[] nums) {
        HashSet<Integer> set = new HashSet<>();
        int index = 0;
        for (int num : nums) {
            if (set.add(num)) {
                nums[index++] = num;
            }
        }
        return index;
    }

    public static void main(String[] args) {
        int[] nums = {1, 1, 2, 2, 3, 4};
        int length = removeDuplicates(nums);
        for (int i = 0; i < length; i++) {
            System.out.print(nums[i] + " ");
        }
    }
}

3. 计数和频率统计

最常见的元素

我们可以使用 HashMap 来记录每个元素的频率,最后找出出现最多的元素。

import java.util.HashMap;
import java.util.Map;

public class MajorityElement {
    public static int majorityElement(int[] nums) {
        HashMap<Integer, Integer> countMap = new HashMap<>();
        for (int num : nums) {
            countMap.put(num, countMap.getOrDefault(num, 0) + 1);
        }

        int majorityElement = nums[0];
        int majorityCount = 0;
        for (Map.Entry<Integer, Integer> entry : countMap.entrySet()) {
            if (entry.getValue() > majorityCount) {
                majorityElement = entry.getKey();
                majorityCount = entry.getValue();
            }
        }
        return majorityElement;
    }

    public static void main(String[] args) {
        int[] nums = {3, 2, 3};
        System.out.println("Majority Element: " + majorityElement(nums));
    }
}

4. 字符串和子串问题

无重复字符的最长子串

使用滑动窗口技术,并用哈希表记录窗口内的字符,更新子串的最大长度。

import java.util.HashMap;

public class LongestSubstring {
    public static int lengthOfLongestSubstring(String s) {
        HashMap<Character, Integer> map = new HashMap<>();
        int left = 0, maxLength = 0;
        
        for (int right = 0; right < s.length(); right++) {
            if (map.containsKey(s.charAt(right))) {
                left = Math.max(left, map.get(s.charAt(right)) + 1);
            }
            map.put(s.charAt(right), right);
            maxLength = Math.max(maxLength, right - left + 1);
        }
        
        return maxLength;
    }

    public static void main(String[] args) {
        String s = "abcabcbb";
        System.out.println("Longest Substring Length: " + lengthOfLongestSubstring(s));
    }
}

5. 动态规划和哈希表的结合

爬楼梯问题

用动态规划和哈希表来优化存储并解决问题。

import java.util.HashMap;

public class ClimbStairs {
    public static int climbStairs(int n) {
        HashMap<Integer, Integer> dp = new HashMap<>();
        dp.put(0, 1);  // 1 way to stay at the ground
        dp.put(1, 1);  // 1 way to step 1 stair
        
        for (int i = 2; i <= n; i++) {
            dp.put(i, dp.get(i - 1) + dp.get(i - 2));
        }
        
        return dp.get(n);
    }

    public static void main(String[] args) {
        int n = 5;
        System.out.println("Ways to climb " + n + " stairs: " + climbStairs(n));
    }
}

6. 字谜问题

字谜判断

使用哈希表来统计字符频率,判断两个字符串是否为字谜。

import java.util.HashMap;

public class CheckInclusion {
    public static boolean checkInclusion(String s1, String s2) {
        if (s1.length() > s2.length()) return false;
        
        HashMap<Character, Integer> countMap = new HashMap<>();
        for (char c : s1.toCharArray()) {
            countMap.put(c, countMap.getOrDefault(c, 0) + 1);
        }

        int left = 0, right = 0, count = countMap.size();
        
        while (right < s2.length()) {
            char rightChar = s2.charAt(right);
            if (countMap.containsKey(rightChar)) {
                countMap.put(rightChar, countMap.get(rightChar) - 1);
                if (countMap.get(rightChar) == 0) count--;
            }
            
            while (count == 0) {
                if (right - left + 1 == s1.length()) return true;
                char leftChar = s2.charAt(left);
                if (countMap.containsKey(leftChar)) {
                    countMap.put(leftChar, countMap.get(leftChar) + 1);
                    if (countMap.get(leftChar) > 0) count++;
                }
                left++;
            }
            
            right++;
        }
        
        return false;
    }

    public static void main(String[] args) {
        String s1 = "ab";
        String s2 = "eidbaooo";
        System.out.println("Is Inclusion: " + checkInclusion(s1, s2));
    }
}

总结

在 Java 中,哈希表(如 HashMapHashSet)提供了高效的查找、去重、计数等操作。在解决算法题时,合理利用哈希能够显著提高效率,尤其是当你面对需要频繁查找或存储某些数据时。

  • 对于查找问题,可以利用哈希表进行快速查找。
  • 对于去重问题,使用哈希集合可以在 O(n) 时间内去除重复元素。
  • 对于频率统计,使用哈希表统计元素的出现次数并可以快速找出最多的元素。
  • 对于子串问题,哈希表常与滑动窗口技术结合使用,能够有效地优化字符串处理。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值