百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 热门文章 > 正文

哈希Hash算法:原理、应用(哈希算法 知乎)

bigegpt 2025-06-28 12:21 2 浏览

原作者:Linux教程,原文地址:「链接」

什么是哈希算法?

哈希算法(Hash Algorithm),又称为散列算法或杂凑算法,是一种将任意长度的数据输入转换为固定长度输出值的数学函数。其输出结果通常被称为

  • 哈希值(Hash Value)
  • 哈希码(Hash Code)
  • 消息摘要(Message Digest)

该类算法在信息安全领域具有广泛应用,主要包括以下几个方面:

  • 数据完整性校验
  • :用于验证数据在传输或存储过程中是否被篡改。
  • 数字签名与认证
  • :作为加密系统的一部分,确保信息来源的真实性和不可否认性。
  • 密码存储
  • :对用户密码进行哈希处理后存储,增强安全性。
  • 快速数据查找
  • :如哈希表结构中用于高效检索数据。

Part1 哈希算法的工作原理

哈希算法通过一个特定的哈希函数(Hash Function)将原始输入数据(明文、文件、字符串等)映射为一个固定长度的二进制串或十六进制字符串,这个过程也称为“哈希运算”或“摘要生成”。

例如:

输入:"Hello, world!"
输出(SHA-256):"315f5bdb76d078c43b8ac0064e4a01646bf518ea4a329b7f9a7a3e68c3f5fc0"

常见哈希算法

Part2哈希算法:从哈希表谈起

在介绍哈希算法之前,我们先从一个与其密切相关且广泛使用的数据结构——哈希表(Hash Table)讲起。理解哈希表的工作原理,有助于我们更好地掌握哈希算法的设计思想及其应用场景。

2.1 数据结构 —— 哈希表(Hash Table)


2.1.1、哈希表的基本概念

哈希表(Hash Table),又称为散列表,是一种基于键值对(Key-Value Pair)存储的数据结构。它通过哈希函数将键(Key)映射为数组索引,从而实现高效的插入、查找和删除操作。

简而言之:

给定一个键 key,我们可以使用哈希函数快速定位其对应的值 value,时间复杂度接近 (1)

2.1.2、哈希表与其他数据结构的效率对比:

虽然数组和链表也可以实现基本的查询功能,但它们在效率上远不如哈希表。下面是三种结构在常见操作上的性能比较:

操作类型

数组

链表

哈希表

添加元素

(1)

(1)

(1)

查询元素

()

()

(1)

删除元素

()

()

(1)

说明:

  • 数组和链表:若未排序,查找和删除都需要遍历整个结构,因此时间复杂度为线性。
  • 哈希表:通过哈希函数直接计算出数据的位置,避免了遍历,使得增删查改都具有常数级的时间复杂度(理想情况下)。

2.1.3、哈希表的核心机制

哈希表的高效性依赖于两个关键组件:

  1. 哈希函数(Hash Function)
  2. 将任意长度的输入(如字符串、整数等)转换为固定长度的数值。
  3. 该数值再通过取模运算确定其在数组中的位置(即“桶”)。
  4. 冲突解决策略
  5. 不同的键可能映射到同一个位置(称为哈希冲突),需要通过链地址法、开放寻址法等方式解决。

哈希表的基本工作流程如下:

  1. 插入操作:输入键 key → 哈希函数计算索引 → 存储值 value 到对应桶中。
  2. 查询操作:输入键 key → 哈希函数计算索引 → 直接访问桶获取值 value
  3. 删除操作:类似查询,找到后进行删除。

哈希表的代码实现

typedef struct {  
    int key;  
    char *val;  
} Pair;  
/* 基于数组实现的哈希表*/  
typedef struct {  
    Pair *buckets[MAX_SIZE];  
} ArrayHashMap;  
/* 构造函数*/  
ArrayHashMap *newArrayHashMap() {  
    ArrayHashMap *hmap = malloc(sizeof(ArrayHashMap));  
    for (int i=0; i < MAX_SIZE; i++) {  
        hmap->buckets[i] = NULL;  
    }  
    return hmap;  
}  
/* 析构函数*/  
void delArrayHashMap(ArrayHashMap *hmap) {  
    for (int i = 0; i < MAX_SIZE; i++) {  
        if (hmap->buckets[i] != NULL) {  
            free(hmap->buckets[i]->val);  
            free(hmap->buckets[i]);  
        }  
    }  
    free(hmap);  
}  
/* 添加操作*/  
void put(ArrayHashMap *hmap, const int key, const char *val) {  
    Pair *Pair = malloc(sizeof(Pair));  
    Pair->key = key;  
    Pair->val = malloc(strlen(val) + 1);  
    strcpy(Pair->val, val);  
    int index = hashFunc(key);  
    hmap->buckets[index] = Pair;  
}  
/* 删除操作*/  
void removeItem(ArrayHashMap *hmap, const int key) {  
    int index = hashFunc(key);  
    free(hmap->buckets[index]->val);  
    free(hmap->buckets[index]);  
    hmap->buckets[index] = NULL;  
}  
/* 获取所有键值对*/  
void pairSet(ArrayHashMap *hmap, MapSet *set) {  
    Pair *entries;  
    int i = 0, index = 0;  
    int total = 0;  
    /* 统计有效键值对数量*/  
    for (i = 0; i < MAX_SIZE; i++) {  
        if (hmap->buckets[i] != NULL) {  
            total++;  
        }  
    }  
    entries = malloc(sizeof(Pair) * total);  
    for (i = 0; i < MAX_SIZE; i++) {  
        if (hmap->buckets[i] != NULL) {  
            entries[index].key = hmap->buckets[i]->key;  
            entries[index].val = malloc(strlen(hmap->buckets[i]->val) + 1);  
            strcpy(entries[index].val, hmap->buckets[i]->val);  
            index++;  
        }  
    }  
    set->set = entries;  
    set->len = total;  
}  
/* 获取所有键*/  
void keySet(ArrayHashMap *hmap, MapSet *set) {  
    int *keys;  
    int i = 0, index = 0;  
    int total = 0;  
    /* 统计有效键值对数量*/  
    for (i = 0; i < MAX_SIZE; i++) {  
        if (hmap->buckets[i] != NULL) {  
            total++;  
        }  
    }  
    keys = malloc(total * sizeof(int));  
    for (i = 0; i < MAX_SIZE; i++) {  
        if (hmap->buckets[i] != NULL) {  
            keys[index] = hmap->buckets[i]->key;  
            index++;  
        }  
    }  
    set->set = keys;  
    set->len = total;  
}  
/* 获取所有值*/  
void valueSet(ArrayHashMap *hmap, MapSet *set) {  
    char **vals;  
    int i = 0, index = 0;  
    int total = 0;  
    /* 统计有效键值对数量*/  
    for (i = 0; i < MAX_SIZE; i++) {  
        if (hmap->buckets[i] != NULL) {  
            total++;  
        }  
    }  
    vals = malloc(total * sizeof(char *));  
    for (i = 0; i < MAX_SIZE; i++) {  
        if (hmap->buckets[i] != NULL) {  
            vals[index] = hmap->buckets[i]->val;  
            index++;  
        }  
    }  
    set->set = vals;  
    set->len = total;  
}  
/* 打印哈希表*/  
void print(ArrayHashMap *hmap) {  
    int i;  
    MapSet set;  
    pairSet(hmap, &set);  
    Pair *entries = (Pair *)set.set;  
    for (i = 0; i < set.len; i++) {  
        printf("%d -> %s\n", entries[i].key, entries[i].val);  
    }  
    free(set.set);  
}

2.2 哈希算法的作用

哈希表之所以能高效运行,核心在于哈希函数的设计质量。而哈希函数正是哈希算法的具体体现之一

哈希算法与哈希表的关系可以总结为:

  • 哈希算法是一种通用算法,将任意数据转化为固定长度的摘要。
  • 哈希函数是哈希算法的一个具体应用,用于哈希表中将键映射为索引。
  • 哈希表是数据结构,依赖哈希函数实现高效的数据访问。

换句话说:

哈希算法是基础,哈希函数是它的具体实现形式,而哈希表则是哈希函数的重要应用场景之一。



数组

链表

哈希表

查找元素

O(n)

O(n)

O(1)

添加元素

O(1)

O(1)

O(1)

删除元素

O(n)

O(1)

O(1)

观察发现,在哈希表中进行增删查改的时间复杂度都是(1) ,非常高效。

我们可以通过一个通用公式来描述哈希算法的行为:

对于任意输入数据 x,哈希函数 H(x) 生成一个输出y=H(x),满足以下基本性质。



Part3哈希函数的通用特性


Part4哈希算法的常见特性(进阶)

除了上述基础特性外,高质量的哈希算法还具备以下几个关键属性:

特性

描述

确定性

相同的输入总是产生相同的输出。这对于校验和验证场景至关重要。

抗碰撞性

在合理时间内难以找到两个不同的输入 x1≠x2,使得H(x1)=H(x2)。

雪崩效应

输入数据的微小变化(如一个比特),应引起输出值的显著变化。这确保了哈希值的随机性和安全性。

固定长度输出

所有哈希算法的输出长度是固定的,不随输入长度而改变。常见的输出长度包括:MD5(128 位)、SHA-1(160 位)、SHA-256(256 位)等。


Part5 哈希算法的典型应用场景

应用领域

具体用途说明

数据完整性校验

比较传输前后文件的哈希值,判断是否被篡改。例如使用 MD5 校验下载文件的完整性。

密码存储与验证

将用户密码通过哈希算法加密后存储,防止明文泄露。通常结合盐值(Salt)提升安全性。

数字签名与认证

对原始数据生成哈希值后再进行加密签名,提高效率并保障数据真实性。

负载均衡与分布式系统

利用一致性哈希算法将请求均匀分布到多个服务器节点上,避免热点问题。

区块链与交易验证

区块链中的每个区块都通过哈希指针链接前一个区块,形成不可篡改的数据链。

在理解了哈希算法的基本概念和常见类型后,我们接下来通过 C++ 编程语言实现两个常见的哈希算法:CRC32 和 SHA-256,并解析其实现原理和应用场景。

5.1 CRC32 算法实现

CRC32(Cyclic Redundancy Check 32)是一种非加密类校验算法,主要用于检测数据传输中的随机错误。其输出为一个 32 位(4 字节)整数。

注意:CRC32 不具备抗碰撞性,不适合用于密码学安全场景。

实现原理:

CRC32 使用一种基于多项式除法的算法来计算校验值。核心步骤如下:

  1. 初始化 CRC 值为 0xFFFFFFFF
  2. 对每个字节依次异或到 CRC 值中。
  3. 进行 8 次位移操作,并根据当前最低位决定是否异或预定义的多项式 0xEDB88320
  4. 最终结果取反得到标准 CRC32 校验值。

C++ 实现代码

#include <iostream>
#include <string>
unsigned int crc32(const std::string& data) {
    unsigned int crc = 0xFFFFFFFF;
    const unsigned int polynomial = 0xEDB88320;
    for (unsigned char byte : data) {
        crc ^= byte;
        for (int i = 0; i < 8; ++i) {
            if (crc & 1) {
                crc = (crc >> 1) ^ polynomial;
            } else {
                crc >>= 1;
            }
        }
    }
    return ~crc;
}
int main() {
    std::string input = "Hello, World!";
    unsigned int hash = crc32(input);
    std::cout << "CRC32: " << std::hex << hash << std::endl;
    return 0;
}

输出示例

CRC32: 1c291ca3

5.2 SHA-256 算法实现

SHA-256 是 SHA-2 系列中的一种安全哈希算法,广泛用于数字签名、密码存储、区块链等安全敏感领域。它将任意长度的输入转换为一个固定长度的 256 位(32 字节)哈希值。

SHA-256 的核心处理流程如下:

  1. 消息填充
  2. :确保输入长度对 512 位整除。
  3. 分块处理
  4. :将数据划分为多个 512 位的消息块。
  5. 初始化哈希值
  6. :使用 8 个固定的初始哈希值(H0~H7)。
  7. 压缩函数
  8. :对每个消息块执行一系列逻辑运算(AND、XOR、NOT、ROTATE)、位移和加法操作。
  9. 最终输出
  10. :将 8 个中间哈希值拼接,生成最终的 256 位哈希值。

使用 OpenSSL 库实现 SHA-256(推荐方式)

OpenSSL 提供了成熟的哈希函数接口,适用于实际开发。

#include <iostream>
#include <iomanip>
#include <sstream>
#include <openssl/sha.h>
std::string sha256(const std::string& data) {
    unsigned char hash[SHA256_DIGEST_LENGTH];
    SHA256(reinterpret_cast<const unsigned char*>(data.c_str()), data.size(), hash);
    std::stringstream ss;
    for (int i = 0; i < SHA256_DIGEST_LENGTH; ++i) {
        ss << std::hex << std::setw(2) << std::setfill('0') << static_cast<int>(hash[i]);
    }
    return ss.str();
}
int main() {
    std::string input = "Hello, World!";
    std::string hash = sha256(input);
    std::cout << "SHA-256: " << hash << std::endl;
    return 0;
}

编译时需链接 OpenSSL:

g++ -o sha256_example sha256_example.cpp -lssl -lcrypto

输出示例

SHA-256: a591a6d40bf420404a011733cfb7b190d62c65bf0bcda32b7748722cfcbd35a5

学习建议

  • 初学者
  • 可尝试手动实现 CRC32 来理解基础哈希机制;
  • 进阶开发者
  • 推荐使用如 OpenSSL、Crypto++ 等成熟库实现 SHA-256;
  • 若需要完全自定义实现 SHA-256,可参考 FIPS 180-4 标准文档逐步编码。

相关推荐

Java 泛型大揭秘:类型参数、通配符与最佳实践

引言在编程世界中,代码的可重用性和可维护性是至关重要的。为了实现这些目标,Java5引入了一种名为泛型(Generics)的强大功能。本文将详细介绍Java泛型的概念、优势和局限性,以及如何在...

K8s 的标签与选择器:流畅运维的秘诀

在Kubernetes的世界里,**标签(Label)和选择器(Selector)**并不是最炫酷的技术,但却是贯穿整个集群管理与运维流程的核心机制。正是它们让复杂的资源调度、查询、自动化运维变得...

哈希Hash算法:原理、应用(哈希算法 知乎)

原作者:Linux教程,原文地址:「链接」什么是哈希算法?哈希算法(HashAlgorithm),又称为散列算法或杂凑算法,是一种将任意长度的数据输入转换为固定长度输出值的数学函数。其输出结果通常被...

C#学习:基于LLM的简历评估程序(c# 简历)

前言在pocketflow的例子中看到了一个基于LLM的简历评估程序的例子,感觉还挺好玩的,为了练习一下C#,我最近使用C#重写了一个。准备不同的简历:image-20250528183949844查...

55顺位,砍41+14+3!季后赛也成得分王,难道他也是一名球星?

雷霆队最不可思议的新星:一个55号秀的疯狂逆袭!你是不是也觉得NBA最底层的55号秀,就只能当饮水机管理员?今年的55号秀阿龙·威金斯恐怕要打破你的认知了!常规赛阶段,这位二轮秀就像开了窍的天才,直接...

5分钟读懂C#字典对象(c# 字典获取值)

什么是字典对象在C#中,使用Dictionary类来管理由键值对组成的集合,这类集合被称为字典。字典最大的特点就是能够根据键来快速查找集合中的值,其键的定义不能重复,具有唯一性,相当于数组索引值,字典...

c#窗体传值(c# 跨窗体传递数据)

在WinForm编程中我们经常需要进行俩个窗体间的传值。下面我给出了两种方法,来实现传值一、在输入数据的界面中定义一个属性,供接受数据的窗体使用1、子窗体usingSystem;usingSyst...

C#入门篇章—委托(c#委托的理解)

C#委托1.委托的定义和使用委托的作用:如果要把方法作为函数来进行传递的话,就要用到委托。委托是一个类型,这个类型可以赋值一个方法的引用。C#的委托通过delegate关键字来声明。声明委托的...

C#.NET in、out、ref详解(c#.net framework)

简介在C#中,in、ref和out是用于修改方法参数传递方式的关键字,它们决定了参数是按值传递还是按引用传递,以及参数是否必须在传递前初始化。基本语义对比修饰符传递方式可读写性必须初始化调用...

C#广义表(广义表headtail)

在C#中,广义表(GeneralizedList)是一种特殊的数据结构,它是线性表的推广。广义表可以包含单个元素(称为原子),也可以包含另一个广义表(称为子表)。以下是一个简单的C#广义表示例代...

「C#.NET 拾遗补漏」04:你必须知道的反射

阅读本文大概需要3分钟。通常,反射用于动态获取对象的类型、属性和方法等信息。今天带你玩转反射,来汇总一下反射的各种常见操作,捡漏看看有没有你不知道的。获取类型的成员Type类的GetMembe...

C#启动外部程序的问题(c#怎么启动)

IT&OT的深度融合是智能制造的基石。本公众号将聚焦于PLC编程与上位机开发。除理论知识外,也会结合我们团队在开发过程中遇到的具体问题介绍一些项目经验。在使用C#开发上位机时,有时会需要启动外部的一些...

全网最狠C#面试拷问:这20道题没答出来,别说你懂.NET!

在竞争激烈的C#开发岗位求职过程中,面试是必经的一道关卡。而一场高质量的面试,不仅能筛选出真正掌握C#和.NET技术精髓的人才,也能让求职者对自身技术水平有更清晰的认知。今天,就为大家精心准备了20道...

C#匿名方法(c#匿名方法与匿名类)

C#中的匿名方法是一种没有名称只有主体的方法,它提供了一种传递代码块作为委托参数的技术。以下是关于C#匿名方法的一些重要特点和用法:特点省略参数列表:使用匿名方法可省略参数列表,这意味着匿名方法...

C# Windows窗体(.Net Framework)知识总结

Windows窗体可大致分为Form窗体和MDI窗体,Form窗体没什么好细说的,知识点总结都在思维导图里面了,下文将围绕MDI窗体来讲述。MDI(MultipleDocumentInterfac...