欢迎光临
我们一直在努力

从零开始的敲代码生活--数据结构篇(哈希表与算法)

一、哈希表基础概念

散列存储(哈希存储):将要存储的元素的关键字和存储位置之间建立起对应关系,这个关系称为哈希函数。

  • 数据存储时,按照哈希函数的映射进行存储
  • 数据查找时,也按照哈希函数的映射进行查找

哈希表(散列表):利用哈希函数计算元素存储位置的数据结构,查找时直接根据关键字计算位置,理想情况下查找时间复杂度可达到 $(O(1))$。

常用术语

  • 哈希函数(散列函数):将元素的关键字映射为存储位置(数组下标)的函数
  • 哈希冲突(碰撞):不同的关键字通过哈希函数计算出相同的存储位置

冲突处理方式

  • 开放地址法:发生冲突时,在哈希表中按某种规则寻找下一个空闲位置
  • 链地址法(拉链法):将哈希表每个位置称为"哈希桶",把冲突的元素以链表的形式挂在同一个桶上,本示例代码采用该方法
  • 哈希表 API

  • 插入数据
  • 查找数据
  • 遍历打印
  • 释放/销毁
  • 示例说明:本示例实现一个"姓名 + 电话"电话簿哈希表,哈希函数按姓名首字母(a‑z)映射到 0‑25 号桶,非字母字符放入 26 号桶(HASH_SIZE‑1),同字母开头的姓名冲突时采用头插法链入同一个桶。

    文件说明

    文件说明
    hash.h 头文件:电话信息结构体、结点结构体定义 + 函数声明
    hash.c 源文件:哈希函数、插入、查找、遍历、释放等功能实现
    main.c 测试 main 函数

    二、哈希表代码

    1. 头文件 hash.h

    #ifndef _HASH_H
    #define _HASH_H
    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #define HASH_SIZE 27 // 哈希表桶的个数:26个字母桶 + 1个其他字符桶

    /* 存储的数据类型:姓名 + 电话 */
    typedef struct data
    {
    char name[32];
    char tel[16];
    }P_info;

    /* 哈希桶结点结构体:数据域 + 指针域 */
    typedef struct node
    {
    P_info data; // 数据域:电话信息
    struct node *pnext; //指针域:下一个结点的地址
    }Node_t;

    extern int inserte_hash(Node_t **phash,P_info data);
    extern void show_hash(Node_t **phash);
    extern int find_hash(Node_t **phash,char name[]);
    extern void free_hash(Node_t **phash);
    #endif

    2. 功能实现 hash.c

    hash_fun 哈希函数

    功能:以姓名首字母为关键字计算哈希值(下标)。小写字母返回 key-'a'(0~25),大写字母返回 key-'A'(0~25),其他字符统一返回 HASH_SIZE‑1(26 号桶)。返回:哈希值(桶下标)。

    int hash_fun(char key)
    {
    if(key >= 'a' && key <= 'z')
    {
    return key-'a';
    }
    else if (key >= 'A' && key <= 'Z')
    {
    return key-'A';
    }
    else
    {
    return HASH_SIZE-1;
    }
    }

    inserte_hash 插入数据

    功能:计算关键字哈希值得到桶下标,采用头插法将新结点插入对应哈希桶(链地址法解决冲突)。返回:0 成功,‑1 失败(malloc 失败)。

    int inserte_hash(Node_t **phash,P_info data)
    {
    Node_t *pinsert = malloc(sizeof(Node_t));
    if(pinsert == NULL)
    {
    printf("mallloc fail\\n");
    return -1;
    }
    int idx = hash_fun(data.name[0]);
    pinsert->data = data;
    pinsert->pnext = phash[idx];
    phash[idx] = pinsert;
    return 0;
    }

    show_hash 遍历打印

    功能:依次遍历每个哈希桶,打印桶中所有结点的姓名与电话。

    void show_hash(Node_t **phash)
    {
    for(int i = 0;i < HASH_SIZE;i++)
    {
    Node_t *ptemp = phash[i];
    while(ptemp != NULL)
    {
    printf("%s,%s\\n",ptemp->data.name,ptemp->data.tel);
    ptemp = ptemp->pnext;
    }
    }
    }

    find_hash 查找数据

    功能:按姓名计算哈希值定位到桶,遍历桶内链表逐个比较姓名,找到则打印结点地址,找不到打印 NotFind。返回:0 成功,‑1 失败(未找到或入参为 NULL)。

    int find_hash(Node_t **phash,char name[])
    {
    if(phash == NULL)
    {
    return -1;
    }
    int idx = hash_fun(name[0]);
    Node_t *ptemp = phash[idx];
    while(ptemp != NULL)
    {
    if(strcmp(ptemp->data.name,name) == 0)
    {
    printf("%p\\n",ptemp);
    return 0;
    }
    ptemp = ptemp->pnext;
    }
    printf("NotFind\\n");
    return -1;
    }

    free_hash 释放哈希表

    功能:遍历所有哈希桶,释放桶内链表上的全部结点。

    void free_hash(Node_t **phash)
    {
    for(int i = 0;i < HASH_SIZE;i++)
    {
    Node_t *pfree = NULL;
    while(phash[i] != NULL)
    {
    pfree = phash[i];
    phash[i] = pfree->pnext;
    free(pfree);
    }
    }
    }

    3. 测试 main 函数 main.c

    #include "hash.h"
    int main(void)
    {
    Node_t *phash[HASH_SIZE] = {NULL};
    P_info data[5] = {{"zhangsan","111111"},
    {"lisi","222222"},
    {"wangwu","333333"},
    {"xiongda","444444"},
    {"xionger","555555"}};
    inserte_hash(phash,data[0]);
    inserte_hash(phash,data[1]);
    inserte_hash(phash,data[2]);
    inserte_hash(phash,data[3]);
    inserte_hash(phash,data[4]);
    show_hash(phash);
    find_hash(phash,data[4].name);
    free_hash(phash);
    return 0;
    }

    4. 编译运行 & 内存检测

    编译:

    gcc main.c hash.c -o hash_demo

    运行程序:

    ./hash_demo

    valgrind 检测内存泄漏:

    写哈希表务必检测内存泄漏,保证每一块 malloc 都有对应的 free

    valgrind –leak-check=full ./hash_demo

    运行输出结果:

    xionger,555555
    xiongda,444444
    wangwu,333333
    lisi,222222
    zhangsan,111111
    000002E404AD9E20

    注:最后一行为 find_hash 打印的结点地址(%p),为程序运行期随机值,每次运行结果不同。

    三、算法基础概念

    算法:解决特定问题的步骤。

    程序设计 = 数据结构 + 算法

    算法的设计要求

  • 正确性:语法正确;合法的输入能得到合理的结果;对非法的输入给出满足要求的规格说明;对精心选择、甚至刁难的测试都能正常运行,结果正确
  • 可读性:便于交流、阅读、理解(高内聚、低耦合)
  • 健壮性:输入非法数据时,能进行相应的处理,而不是产生异常
  • 高效率(时间复杂度)
  • 低存储(空间复杂度)
  • 空间复杂度:算法执行过程中额外开辟的空间随数据量 n 的变化关系。

    • $(O(1))$:额外空间与数据量无关
    • $(O(n))$:额外空间随数据量线性增长

    算法时间复杂度:执行这个算法所花时间的度量。将数据量增长和时间增长用函数表示出来,这个函数就叫做时间复杂度。一般用大 O 表示法:$(O(n))$ —— 时间复杂度是关于数据量 n 的一个函数。随着 n 的增加,时间复杂度增长较慢的算法时间复杂度低。

    时间复杂度的计算规则

  • 用常数 1 取代运行时间中的所有加法常数
  • 在修改后的运行函数中,只保留最高阶项
  • 如果最高阶存在且系数不是 1,则去除这个项相乘的常数
  • 常见时间复杂度示例

    示例代码次数时间复杂度
    交换两个变量的值(常数次操作) 1 $(O(1))$
    for(i=0; i<n; i+=2) (循环 n 次) n $(O(n))$
    for(i=1; i<n; i*=2) ($2^x = n$ ,$x = logn$) logn $(O(logn))$
    外层循环 n 次,内层 i*=2 循环 n*logn $(O(nlogn))$
    双重 for 循环(0+1+2+…+n‑1 = (n+1)n/2) $n^2$ $O(n^2)$

    复杂度大小比较:

    $$(O(1)) < (O(logn))<(O(n))<O(n logn)<O(n^2)<O(n^3)<O(2^n)<O(n !)<O(n^n)$$

    四、常用排序和查找算法

    常用排序算法

  • 选择排序
  • 冒泡排序
  • 插入排序
  • 希尔排序
  • 快速排序
  • 查找算法

  • 二分查找
    • 前提条件:序列必须有序
    • 时间复杂度:$(O(logn))$
  • 插入排序

    思想:将待排的数据插入到一个已有序的序列中,确保每次插入之后该序列仍然有序

    • 时间复杂度:$O(n^2)$
    • 空间复杂度:$(O(1))$
    • 稳定性:稳定的

    希尔排序

    思想:将待排序列根据增量划分成若干个子序列,分别对这些子序列进行插入排序。增量由大到小逐步递减,当增量减为 1 时整个序列基本有序,再做一次插入排序完成排序

    • 时间复杂度:$(O(nlogn)) \\sim O(n^2)$
    • 空间复杂度:$(O(1))$
    • 稳定性:不稳定

    void shell_sort(int *a, int len)
    {
    int inc = 0;
    int i = 0, j= 0, tmp = 0;
    for (inc = len/2; inc > 0; inc /= 2)
    {
    for (i = inc; i < len; ++i)
    {
    tmp = a[i];
    j = i;
    while (j >= inc && tmp < a[j-inc])
    {
    a[j] = a[j-inc];
    j -= inc;
    }
    a[j] = tmp;
    }
    }
    }

    快速排序

    思想:选取基准值,从两端向中间比较,比基准值大的放在序列的右边,比基准值小的放在序列的左边;经过一趟排序,优先排好基准值;再对基准值左右两个区域重复以上操作,直到整个数组有序

    • 时间复杂度:$(O(nlogn))$
    • 空间复杂度:$(O(logn))$
    • 稳定性:不稳定

    void quick_sort(int *a, int begin, int end)
    {
    if (begin >= end)
    {
    return ;
    }
    int i = begin;
    int j = end;
    int key = a[i];
    while (i < j)
    {
    while (i < j && key <= a[j])
    –j;
    a[i] = a[j];
    while (i < j && key >= a[i])
    ++i;
    a[j] = a[i];
    }
    a[i] = key;
    quick_sort(a, i+1, end);
    quick_sort(a, begin, i-1);
    }

    排序算法汇总表

    算法思想时间复杂度空间复杂度稳定性
    选择排序 每趟从未排序区间选出最小(大)值放到已排序区间末尾 $O(n^2)$ $(O(1))$ 不稳定
    冒泡排序 相邻元素两两比较,大(小)的逐步上浮(下沉) $O(n^2)$ $(O(1))$ 稳定
    插入排序 将待排数据插入已有序序列,保持有序 $O(n^2)$ $(O(1))$ 稳定
    希尔排序 按增量分组分别插入排序,增量递减至 1 $(O(nlogn))\\sim O(n^2)$ $(O(1))$ 不稳定
    快速排序 选取基准值分区,递归排序左右区间 $(O(nlogn))$ $(O(logn))$ 不稳定
    赞(0)
    未经允许不得转载:171主机测评 » 从零开始的敲代码生活--数据结构篇(哈希表与算法)
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址