哈希表与算法时间复杂度

一,哈希表   


(Hash Table),它通过哈希函数将键值映射到特定的数组索引,从而实现高效的查找、插入和删除操作。其核心思想是将数据直接存储到具有固定大小的数组中,通过哈希函数计算出每个数据的存储位置。

主要特性

哈希函数:哈希函数用于将输入的键(通常是字符串或数字)转换为数组中的索引。理想的哈希函数应尽量避免不同的键映射到同一索引(称为冲突)。

冲突处理:

开放地址法:在发生冲突时,通过线性探查、二次探查或双重哈希等方式,寻找数组中下一个空闲位置。
链地址法:在每个哈希表的索引处,维护一个链表或其他容器来存储所有具有相同哈希值的元素。
时间复杂度:

查找、插入和删除的平均时间复杂度为 O(1)O(1)O(1),但在最坏情况下(大量冲突)可能退化为 O(n)O(n)O(n)。
负载因子:负载因子是哈希表中存储的元素数量与哈希表大小的比值。当负载因子过大时,哈希表的性能会下降,通常通过扩展哈希表(重新哈希)来解决。

哈希表的应用


数据库索引
缓存(如 LRU Cache)
唯一性检测(如查找重复项)
字典/映射实现

优缺点

优点:

查找、插入和删除的平均时间复杂度为常数时间 O(1)O(1)O(1)。
适合快速查找和存储大量数据。
缺点:

当哈希函数设计不当或负载因子过大时,性能会急剧下降。
存在内存浪费,特别是在使用开放地址法时,需要额外的存储空间。
哈希冲突
发生在两个不同的键通过哈希函数映射到相同的数组索引时。为了解决哈希冲突,常见的解决办法主要有以下两种:开放地址法和链地址法,每种方法又有不同的变种和策略。

1. 链地址法(Separate Chaining)


链地址法是最常见的哈希冲突解决方案。它通过在哈希表的每个索引处维护一个链表(或其他容器),当多个键被映射到同一索引时,直接将它们放入这个链表中。

优点:
简单直观,容易实现。
不受表大小限制,可以处理超过哈希表容量的数据。
缺点:
如果冲突过多,链表长度变长,查找效率会退化到 O(n)O(n)O(n)。
改进方法:
使用自平衡二叉搜索树或跳表代替链表,从而在冲突严重时保持较好的性能。

2. 开放地址法(Open Addressing)


开放地址法通过在哈希表中寻找下一个可用的空闲位置来存储冲突的元素。它不使用外部链表,而是在数组内部解决冲突。

2.1 线性探查(Linear Probing)
当发生冲突时,按照线性顺序(即每次向前移动一格)依次检查下一个位置,直到找到一个空闲的槽位。

公式:h(k, i) = (h(k) + i) % m,其中 i 表示冲突次数,m 为哈希表大小。

优点:

实现简单。
连续数据的访问具有较高的缓存命中率。
缺点:

容易产生主堆积现象(primary clustering):即多个连续的空位被占用后,新的元素很容易探查到这些连续区域,进一步加剧冲突。

二,相关函数接口

1.头文件

#ifndef __HASH_H__  // 防止头文件重复包含,定义唯一的头文件保护符。
#define __HASH_H__
 
#include <head.h>  // 包含标准或自定义的头文件,用于提供头文件的依赖。
 
#define HASH_SIZE 27  // 定义哈希表的大小,这里使用 27 个槽位(26 个字母 + 1 个非字母字符槽位)。
 
/**
 * @brief 定义存储的数据类型,每个哈希节点存储一个用户的姓名和电话。
 */
typedef struct per
{
    char name[64];  // 用户的姓名,最多 64 个字符。
    char tel[32];   // 用户的电话号码,最多 32 个字符。
} HsDatetype;
 
/**
 * @brief 定义哈希表节点,每个节点包含用户数据和指向下一个节点的指针(用于解决冲突时的链表)。
 */
typedef struct hashnode
{
    HsDatetype data;        // 该节点存储的用户数据(姓名和电话)。
    struct hashnode *pnext; // 指向下一个节点的指针,用于处理哈希冲突(链表法)。
} Hsnode_t;
 
/**
 * @brief 计算哈希值的函数,根据输入字符返回对应的哈希表索引。
 * @param key 输入的字符(通常是姓名的首字母)。
 * @return 返回计算得到的哈希表索引。
 */
int hashfuction(char key);
 
/**
 * @brief 向哈希表中插入一个数据。
 * @param data 要插入的用户数据(包含姓名和电话号码)。
 * @return 插入成功返回 0,失败返回 -1。
 */
int insert_hatable(HsDatetype data);
 
/**
 * @brief 遍历哈希表,输出所有存储的数据。
 * @return 成功返回 0。
 */
int traverse_table();
 
/**
 * @brief 查找哈希表中是否存在指定名字的用户数据。
 * @param name 要查找的名字。
 * @return 如果找到,返回指向该节点的指针;如果未找到,返回 NULL。
 */
Hsnode_t *fine_table(char *name);
 
/**
 * @brief 删除哈希表中指定名字的用户数据,并将删除的数据存储到指定指针中。
 * @param name 要删除的名字。
 * @param data 用于存储删除的数据的指针。
 * @return 成功删除返回 1,未找到返回 0。
 */
int delete_hatable(char *name, HsDatetype *data);
 
/**
 * @brief 删除哈希表中的所有数据,释放内存。
 * @return 成功返回 0。
 */
int delete_table();
 
#endif  // __HASH_H__ 结束头文件保护符。

2.hash.c

#include "hash.h"  // 包含哈希表相关的头文件,定义了数据类型和常量(如 HASH_SIZE)。
 
// 定义哈希表为全局变量,每个位置存储指向链表头节点的指针,初始化为NULL。
Hsnode_t *hashtable[HASH_SIZE] = {NULL};
 
/**
 * @brief 哈希函数,将字符转换为哈希表的索引。
 * @param key 输入的字符(通常是姓名的首字母)。
 * @return 返回该字符在哈希表中的索引。
 */
int hashfuction(char key)
{
    // 如果是小写字母,将其转换为从 0 开始的索引 ('a' -> 0, 'b' -> 1, ...)。
    if(key >= 'a' && key <= 'z')
    {
        return key - 'a';
    }
    // 如果是大写字母,也转换为从 0 开始的索引 ('A' -> 0, 'B' -> 1, ...)。
    else if(key >= 'A' && key <= 'Z')
    {
        return key - 'A';
    }
    // 如果不是字母字符,则返回哈希表的最后一个位置。
    else
    {
        return HASH_SIZE - 1;
    }
}
 
/**
 * @brief 向哈希表中插入数据。
 * @param data 要插入的用户数据(包含姓名和电话信息)。
 * @return 成功返回 0,失败返回 -1。
 */
int insert_hatable(HsDatetype data)
{
    // 根据名字的第一个字符计算哈希值,得到存储位置。
    int addr = hashfuction(data.name[0]);
 
    // 为新节点分配内存空间,存储数据。
    Hsnode_t *pnode = (Hsnode_t*)malloc(sizeof(Hsnode_t));
    if(NULL == pnode)  // 如果内存分配失败,打印错误并返回 -1。
    {
        perror("malloc fail\n");
        return -1;
    }
 
    // 初始化新节点的指针和数据。
    pnode->pnext = NULL;
    pnode->data = data;
 
    // 如果当前哈希表位置为空,将新节点直接插入此处。
    if(hashtable[addr] == NULL)
    {
        hashtable[addr] = pnode;
        return 0;
    }
 
    // 如果哈希表位置已有节点,则需要按字母顺序插入到链表中。
    Hsnode_t *p = hashtable[addr];
 
    // 如果新节点应该插入到链表头部(字母顺序更小),则将其作为新的头节点。
    if(strcmp(p->data.name, data.name) >= 0)
    {
        pnode->pnext = p;
        hashtable[addr] = pnode;
        return 0;
    }
 
    // 否则,找到链表中的正确位置,保持字母顺序。
    while(p->pnext != NULL && strcmp(p->pnext->data.name, data.name) < 0)
    {
        p = p->pnext;
    }
 
    // 将新节点插入链表中,维护链表顺序。
    pnode->pnext = p->pnext;
    p->pnext = pnode;
    return 0;
}
 
/**
 * @brief 遍历哈希表,打印所有存储的用户信息。
 * @return 成功返回 0。
 */
int traverse_table()
{
    printf("\n");
    // 遍历哈希表的每一个槽位。
    for(int i = 0; i < HASH_SIZE; ++i)
    {
        if(hashtable[i] == NULL)  // 如果当前位置为空,跳过。
        {
            continue;
        }
 
        Hsnode_t *p = hashtable[i];
        printf("%c \n", i + 'a');  // 打印当前槽位对应的字母。
 
        // 遍历链表,打印每个节点的用户数据(姓名和电话)。
        while(p != NULL)
        {
            printf("%s  %s \n", p->data.name, p->data.tel);
            p = p->pnext;
        }
        printf("\n");
    }
    return 0;
}
 
/**
 * @brief 查找哈希表中是否存在指定名字的用户信息。
 * @param name 要查找的名字。
 * @return 返回指向找到节点的指针,未找到返回 NULL。
 */
Hsnode_t *fine_table(char *name)
{
    // 根据名字的第一个字符计算哈希地址。
    int addr = hashfuction(name[0]);
 
    // 遍历哈希表中对应的链表,寻找匹配的名字。
    Hsnode_t *p = hashtable[addr];
    while(p != NULL)
    {
        // 如果找到名字匹配的节点,返回该节点。
        if(!strcmp(p->data.name, name))
        {
            return p;
        }
        p = p->pnext;
    }
    return NULL;  // 未找到返回 NULL。
}
 
/**
 * @brief 删除哈希表中指定名字的用户数据。
 * @param name 要删除的名字。
 * @param data 保存被删除的节点数据(输出参数)。
 * @return 成功返回 1,未找到返回 0。
 */
int delete_hatable(char *name, HsDatetype *data)
{
    // 根据名字的第一个字符计算哈希地址。
    int addr = hashfuction(name[0]);
 
    Hsnode_t *p = hashtable[addr];
    if(hashtable[addr] == NULL)  // 如果当前哈希表位置为空,返回 0。
    {
        return 0;
    }
 
    // 如果第一个节点就是要删除的节点,直接删除它。
    if(!strcmp(p->data.name, name))
    {
        *data = p->data;  // 保存删除的节点数据。
        hashtable[addr] = p->pnext;  // 更新哈希表头指针。
        Hsnode_t *q = p;
        free(q);  // 释放节点内存。
        return 0;
    }
 
    // 否则,遍历链表,查找要删除的节点。
    while(p->pnext != NULL)
    {
        if(!strcmp(p->pnext->data.name, name))
        {
            *data = p->pnext->data;  // 保存删除的节点数据。
            Hsnode_t *q = p->pnext;
            p->pnext = p->pnext->pnext;  // 更新链表指针。
            free(q);  // 释放节点内存。
            return 1;
        }
        p = p->pnext;
    }
    return 0;  // 如果未找到节点,返回 0。
}
 
/**
 * @brief 删除整个哈希表中的所有节点,释放所有内存。
 * @return 成功返回 0。
 */
int delete_table()
{
    // 遍历哈希表的每一个槽位。
    for(int i = 0; i < HASH_SIZE; ++i)
    {
        if(hashtable[i] == NULL)  // 如果当前槽位为空,跳过。
        {
            continue;
        }
 
        Hsnode_t *p = hashtable[i];
        // 释放该槽位下链表中的所有节点。
        while(p != NULL)
        {
            hashtable[i] = p->pnext;  // 更新链表指针。
            Hsnode_t *q = p;
            p = p->pnext;
            free(q);  // 释放节点内存。
        }
    }
    return 0;
}

3.函数验证

#include "hash.h"  // 包含哈希表相关的头文件,提供数据结构和函数声明。
 
/**
 * @brief 主函数,程序入口。
 * @param argc 命令行参数的数量。
 * @param argv 命令行参数的列表。
 * @return 程序的退出状态码。
 */
int main(int argc, char *argv[])
{
    // 初始化多个用户数据,包括姓名和电话号码,使用结构体数组存储。
    HsDatetype pers[] = {{"zhansan", "110"}, {"lisi", "120"},
                         {"wangwu", "119"}, {"longjunlin", "114"},
                         {"maqi", "10086"}, {"waa", "156"}};
 
    // 将每个用户数据插入哈希表。
    insert_hatable(pers[0]);
    insert_hatable(pers[1]);
    insert_hatable(pers[2]);
    insert_hatable(pers[3]);
    insert_hatable(pers[4]);
    insert_hatable(pers[5]);
 
    // 遍历并打印当前哈希表中的所有数据。
    traverse_table();
 
    printf("\n**************\n");
 
    // 查找哈希表中是否有 "wangwu" 的数据。
    Hsnode_t *p = fine_table("wangwu");
    if(p != NULL)  // 如果找到,则打印该用户的信息。
    {
        printf("%s %s\n", p->data.name, p->data.tel);
    }
 
    printf("\n**************\n");
 
    // 删除哈希表中的 "longjunlin" 数据,并将删除的节点数据保存到 `data` 中。
    HsDatetype data;
    int ret = delete_hatable("longjunlin", &data);
    if(1 == ret)  // 如果成功删除,打印删除的用户信息。
    {
        printf("%s %s\n", data.name, data.tel);
    }
 
    printf("\n**************\n");
 
    // 再次遍历并打印当前哈希表中的所有数据,显示删除后的结果。
    traverse_table();
 
    // 删除整个哈希表,释放所有节点的内存。
    delete_table();
 
    return 0;  // 程序正常退出。
}

三,算法时间复杂度

1.冒泡排序


冒泡排序的基本原理

对存放原始数组的数据,按照从前往后的方向进行多次扫描,每次扫描都称为一趟。当发现相邻两个数据的大小次序不符合时,即将这两个数据进行互换,如果从小大小排序,这时较小的数据就会逐个往前移动,好像气泡网上漂浮一样。

冒泡排序的特点:

升序排序当中每一轮比较会把最大的数沉到最底(这里以从小到大为例),所有相互比较的次数每一轮会比前一轮少一次。

冒泡排序的时间复杂度:O(n^2)

空间复杂度:1

算法稳定性:×

O(N)和真实的计算时间成正比

从前到后执行一轮要n次,O(N) N指的是数据的规模.

2.插入排序

基本原理

插入排序(英语:Insertion Sort)是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。插入排序在实现上,在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。

插入排序的时间复杂度:O(N^2)

空间复杂度:1

算法稳定性:√

一个数插入平均需要 O(二分之N),但是常数可以省略

插入排序的缺点: 后面的数值越小,我们需要移动的次数越多,进而影响整个程序

3.选择排序


基本操作:

选择排序(select sorting)也是一种简单的排序方法。

它的基本思想是:第一次从arr[0到]arr[n-1]中选取最小值,与arr[0]交换,第二次从arr[1]到arr[n-1]中选取最小值,与arr[1]交换,第三次从arr[2]到arr[n-1]中选取最小值,与arr[2]交换,…,第i次从arr[i-1]arr[n-1]中选取最小值,与arr[i-1]交换,…, 第n-1次从arr[n-2]~arr[n-1]中选取最小值,与arr[n-2]交换,总共通过n-1次,得到一个按排序码从小到大排列的有序序列。

简单选择排序 时间复杂度:O(N^2)

空间复杂度:1

算法稳定性:×

6.快速排序


基本思想

快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。

算法描述

快速排序使用分治法来把一个串(名单)分为两个子串(子列表)具体算法描述如下:

会把数组当中的一个数当成基准数
一般会把数组中最左边的数当成基准数,然后丛两边进行检索。丛右边检索比基准数小的,
然后左边检索比基准数大的。如果检索到了,就停下,然后交换这两个元素,然后继续检索。

快速排序的时间复杂度:O(nLog(n))

空间复杂度:1

算法稳定性:×

  • 11
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值