数据结构-链表、队列、栈、哈希表

数据结构指的是任意长度、类型的数据对应的字节在内存中的存放结构。内存存储单元的最小单位是Byte,内存物理上是连续的、无差别的。但是软件可以通过不一样的使用方式来填充、操作内存。例如数组中,数据是顺序存放至内存,使用时则是按照下标去索引。以上提到的主要可以概括为两点:1、存储方式 2、使用方式,按照存储方式分类可以分为:顺序存储与离散存储;按照使用方式可分为:顺序使用、随机使用。当然顺序存储并不意味着只能顺序使用,离散存储也不是只能随机使用,两者是可以自由组合的,而最终组合的产物就被称为数据结构。
例如为人熟知的栈、队列,这些数据结构的共同点是只能顺序使用,而栈与队列的唯一区别是“栈是喝多了吐,队列是吃多了拉”,这其实只是顺序使用的进一步细分而已。至于栈、队列的存储方式则是不固定的,可以使用数组这种顺序存储作为栈、队列的存储方式,也可以使用链表这种离散的存储作为栈、队列存储方式。但是通常来说,数组大小在编译时大小已固定,既并不能减小也不能增大,相比于链表的动态创建与销毁显得不是很灵活。
我自己是按照“使用方式”来组织学习的,先学习顺序使用的数据结构,存储方式则全部使用离散存储。

链表

首先学习链表,因为链表进一步加工就可以变化为栈或者队列。链表的基础定义不多啰嗦,这里重点了解下linux内核实现的一种类型无关的链表与普通链表的差异。
一般来说,设计一个链表,需要首先定义数据节点:节点中包含需存放的数据以及指向该节点的前向、后向指针,如下:

 struct node {
     int val;
     struct node *prev;
     struct node *next;
 };

如果实现的是一个循环双向链表,那么对应的图示应该是:
在这里插入图片描述
按照上述方法设计的链表存在一个缺点:一种数据节点的类型是特定的,如果要存放另一种数据类型则需要重新实现节点。
linux内核转换思路,并不把数据类型作为节点设计的重点,每个节点中的前向、后向指针类型固定,且指向下一个节点中的前、后向指针,也就是说,完全将指针串为一个链表,用户使用时,只需要将指针结构体放到自己的数据结构即可:
在这里插入图片描述
这样可以实现任意数据类型均使用同一套链表接口,但是链表中的各个指针并没有指向有效数据,那么如何通过一个链表中的指针得到有效数据呢?用下面这个方法:

#define OFFSETOF(type, member)        (size_t)&(((type *)0)->member)

#define container_of(ptr, type, member) ({                      \
        const typeof( ((type *)0)->member ) *__mptr = (ptr);    \
        (type *)( (char *)__mptr - OFFSETOF(type,member) );})

在这里插入图片描述
上面这个复杂的宏定义实现的作用其实是:传入一个结构体类型,以及该结构体中包含的链表指针,就可以根据结构体大小以及指针地址向上推导出该指针所在结构体的首地址!

栈在编程语言的实现中是十分有用的,典型例子就是函数的调用栈,函数栈的介绍参考:函数栈工作过程汇编级函数栈实现原理
栈的核心规则是,先进后出,类似于装水的水桶,先装入的水在最低部,后装入的水在上部,使用时自然先使用上部的水。类似于函数调用,一个函数调用一个子函数,子函数再次调用一个子函数,返回时自然先返回第一个子函数,最后返回到主函数。而每级函数都需要有自己的内存用于存放临时变量、传参、返回地址等数据,因此使用栈的形式完全与需求匹配,最先进入栈的数据是主函数的,这些数据一定是要等到其子函数执行完毕返回后才能被使用到,所以是先进后出的,同理最后一级子函数的数据会被首先使用,而它们是被最后压如栈的,同样是后进先出。
入栈
在这里插入图片描述

但是,除了上述应用场景,在实际的编程中栈数据结构貌似没有太多的用武之地,如果不考虑数据的实效性的话,可以将栈设计成一个容器,存放生产者生产过剩的产品,消费者使用最后生产的产品。无论如何,先来学习下栈的实现吧。
链栈相比于链表仅有一处约束:只能从最尾部pop元素。也就是对用户只开放尾删除节点并返回该节点的接口,其它同链表的原始操作:

void list_del_tail(struct list_head *head)
{
    __list_del(head->prev->prev,head);
}

void stack_creat(struct list_head *list)
{
    INIT_LIST_HEAD(list);
}

void stack_push(struct list_head *new, struct list_head *head)
{
    list_add_tail(new,head);
}

void stack_pop(struct list_head *head)
{
    struct list_head *list = head->prev; /* 保存链表的最后节点 */

    list_del_tail(head); /* 尾删法 */
    
    INIT_LIST_HEAD(list); /* 重新初始化删除的最后节点,使其指向自身 */

}

int get_satck_size(struct list_head *head)
{
    struct list_head *pos;
    int size = 0;
    
    if (head == NULL) {
        return -1;
    }
    
    list_for_each(pos,head) {
        size++;
    }

    return size;
}

队列

队列相比于栈,理解起来更简单,其实就是一种先进先出的队列结构,因此非常适合用来设计诸如消息队列之类的小组件。具体实现上,若同样基于linux内核链表来做,只做一个限制:提供pop队列的函数接口,该接口使用首删法操作链表

线性表(链表)、栈、队列的三种顺序型数据结构理解起来很容易,可参考我移植出来的内核链表以及对三种数据结构的实现:https://github.com/lupingguang/c_test/tree/master/msg_queue

哈希表

传统的链表在插入数据时效率非常高,但是在查询数据时,只能按照链表的顺序去遍历,这与数组使用下标直接定位元素是存在明显性能差距的。但是数组又不具有链表快速插入的特性,所以能够想到的一种优化方法是:基于数组与链表设计一种顶层数据结构,这种数据结构就是哈希表。哈希表基础概念
在这里插入图片描述

key-value

数据本身的内容:key与value,key-value是一一对应的,类似于c++中的pairs,我们在查询数据时,都是基于key值来搜索的。但是由于哈希表的存放过程并不完全是顺序存放的,因此在存放时就需要考虑key值到bucket下标的转化。

//定义15个数据,key为元素内容,value为下标值
unsigned char coef[15] = {
    0x01, 0x02, 0x04, 0x08,0x10, 0x20, 0x40, 0x80, 0x1d, 0x3a, 0x74, 0xe8, 0xce, 0x87, 0x13,
};

哈希函数

哈希函数的作用其实就是实现key与bucket index的转化,哈希函数可能对多个key值转化后得到相同的bucket下标,这种情况一般都会出现,因为bucket大小有限。哈希函数必备特性如下:

  1. 输入域无穷,输出域有限。例如:有无穷多个(在工程中可以具体到多少个,例如1000)输入参数经过hash函数映射后得到有限的输出域{1,2,3,4}。
    这一点表达的是,无论数据量多大,最终转换出的bucket下标范围是固定的
  2. 输入参数确定,经过hash函数映射出的返回值一样。(不是随机函数,不同的输入参数可能得到相同的返回值)。
    hash函数每次转化的结果要确定,否则后续的查找就会出错
  3. 输入域上的值经过函数值映射后会几乎均等的分布在输出域上。
    这一点最重要,毕竟bucket的所有位置最好是全部被使用到,而且存放的数据量要基本一致
    基于以上三点,可选择如下一个典型的哈希函数使用:
static inline int hash_func(unsigned char k)
{
    int a, b, p, m;
    a = 104;
    b = 52;
    p = 233;
    m = HASH_NUMBER;
    return ((a * k + b) % p) % m;
}

bucket

哈希表的设计思路十分明了,首先需要根据内存情况定义bucket的尺寸,一般来讲是使用数组,若数组大小等于待存放数据的数量,那么数组的每个位置可直接存放数据本身,这种情况下哈希表已经退化为数组了。所以通常来讲可以将bucket大小适当减小,以降低插入数据时的数组性。当bucket大小小于数据数量时,那么某些bucket位置存放的应该是一个链表的首地址,该链表上可挂载多个数据。Linux内核有一个哈希表的半成品,其实就是实现了上述分析中的链表部分。

//定义数据结构体,存放key value以及构造链表所需的hlist_node
struct q_coef
{
    unsigned char coef;
    unsigned char index;
    struct hlist_node hash;
};
//bucket大小=10,小于数据量
struct q_coef q_coef_list[10];
//移植Linux内核中的hash_list.h文件,并定义一个同样大小的hash链表头数组,每个bucket位置使用一个list_head
struct hlist_head hashtbl[10];
//数据存放到hash表中
static void hash_init(void)
{
      int i, j;
    struct q_coef *dy_q_coef = NULL;
    //首先初始化bucket对应的10个链表头
    //再初始化各个bucket中的hlist_node成员,并将key值清为0xff
    for (i = 0 ; i < HASH_NUMBER ; i++) {
        INIT_HLIST_HEAD(&hashtbl[i]);
        INIT_HLIST_NODE(&q_coef_list[i].hash);
        q_coef_list[i].coef = 0xff;
        q_coef_list[i].index = 0;
    }
    
    for (i = 0 ; i < 15 ; i++) {
    	//由哈希函数根据key值计算bucket下标
        j = hash_func(coef[i]);
        //如果该bucket下标对应的位置未使用,则将数据存入,同时将该bucket元素放入哈希链表
        if(q_coef_list[j].coef == 0xff)
        {
                q_coef_list[j].coef = coef[i];
                q_coef_list[j].index = i;
                hlist_add_head(&q_coef_list[j].hash, &hashtbl[j]);
                //printf("j1= %d, i =%d addr:%p\n",j,i, &q_coef_list[j]);
        }
        //如果该bucket下标对应的位置已使用,则动态分配一个数据结构体将数据存入,同时将该数据结构体放入哈希链表
        else
        {
                dy_q_coef = (struct q_coef *)malloc(sizeof(struct q_coef));
                dy_q_coef->coef = coef[i];
                dy_q_coef->index = i;
                INIT_HLIST_NODE(&(dy_q_coef->hash));
                //printf("j2= %d, i =%d add:%p\n",j,i,(dy_q_coef));
                hlist_add_head(&(dy_q_coef->hash), &hashtbl[j]);

        }
     }

}

添加数据完毕后,可以打印所有数据出来,看是否可索引到。哈希表的索引原则是:首先由key计算bucket下标,若bucket某位置存放唯一数据,则直接找到;若计算的bucket下标冲突,代表该位置存放了多个数据成员,那么需要在bucket某位置中找到链表头,从链表中再一次匹配key值(数据的key是唯一的):

static void hash_test(void)
{
    int i, j;
    struct q_coef *q;
    struct hlist_node *hn;

    //桶定义为10大小,哈希函数计算得到的key冲突
    for (i = 0 ; i < 15 ; i++)
    {
        j = hash_func(coef[i]);
        //不冲突的key,那么hashtbl只会指向一个node,node的next为NULL
        //若冲突,那么hashtbl指向以该key为key的首个node,node的next继续指向下一个相同key的元素
        //针对同一个tbl位置(key冲突)存储多个value情况,会打印出该key对应的所有value(该tbl位置存储的链表上所有的元素)
        //判断链表结束的条件是:node节点的next成员指向NULL(后续无节点)
        hn = hashtbl[j].first;
        struct hlist_node *temp_hn = NULL;
        do
        {
                q = container_of(hn, struct q_coef, hash);
                printf("bucket-index=%d key=0x%02x value=%d addr:%p\n",j,  q->coef, q->index, q);
                temp_hn = hn;
                hn = hn->next;
        }
        while(temp_hn->next != NULL);
    }
}

打印:

bucket-index=6 key=0x3a value=9 addr:0xd89440
bucket-index=6 key=0x01 value=0 addr:0x601170
bucket-index=7 key=0x02 value=1 addr:0x601188
bucket-index=2 key=0x87 value=13 addr:0xd89480
bucket-index=2 key=0x04 value=2 addr:0x601110
bucket-index=5 key=0x10 value=4 addr:0xd89420
bucket-index=5 key=0x08 value=3 addr:0x601158
bucket-index=5 key=0x10 value=4 addr:0xd89420
bucket-index=5 key=0x08 value=3 addr:0x601158
bucket-index=8 key=0x20 value=5 addr:0x6011a0
bucket-index=4 key=0x13 value=14 addr:0xd894a0
bucket-index=4 key=0x40 value=6 addr:0x601140
bucket-index=3 key=0x80 value=7 addr:0x601128
bucket-index=9 key=0x1d value=8 addr:0x6011b8
bucket-index=6 key=0x3a value=9 addr:0xd89440
bucket-index=6 key=0x01 value=0 addr:0x601170
bucket-index=0 key=0xce value=12 addr:0xd89460
bucket-index=0 key=0x74 value=10 addr:0x6010e0
bucket-index=1 key=0xe8 value=11 addr:0x6010f8
bucket-index=0 key=0xce value=12 addr:0xd89460
bucket-index=0 key=0x74 value=10 addr:0x6010e0
bucket-index=2 key=0x87 value=13 addr:0xd89480
bucket-index=2 key=0x04 value=2 addr:0x601110
bucket-index=4 key=0x13 value=14 addr:0xd894a0
bucket-index=4 key=0x40 value=6 addr:0x601140

其中bucket的0、2、4、5、6位置分别存放了两个数据,1、3、7、8、9存放1个数,共15个数据
0、2、4、5、6位置都打印了两次是因为这几个位置都出现过哈希冲突。总之,哈希表的查找规则是:先定位数组位置,再遍历链表

增、删、查操作

对于哈希表的查询,仍然使用哈希函数将key转化为bucket下标,再在对应的链表上遍历查找该key,这一过程保留了部分数组索引定位的高效,比单纯的链表遍历效率要高

struct q_coef* hash_query(unsigned char ckey)
{
        struct q_coef *q;
        struct hlist_node *hn;
        struct hlist_node *temp_hn = NULL;
        struct q_coef *hash_node = NULL;
        int j = hash_func(ckey);
        hn = hashtbl[j].first;

        do
        {
               q = container_of(hn, struct q_coef, hash);
               if(q->coef == ckey)
               {
                        //printf("find ckey =%d, in bucket index %d\n", ckey, j);
                        hash_node = q;
                        break;
               }
               temp_hn = hn;
               hn = hn->next;
        }
        while(temp_hn->next != NULL);
        return hash_node;
}

增删操作则完全是对链表元素的增删(bucket大小已固定),避开数组无法动态改变容量的不便

int hash_delete(unsigned char key)
{
        struct q_coef* hash_node = hash_query(key);
        if(hash_node != NULL)
        {
                printf("delete key =0x%02x, in bucket\n", hash_node->coef);
                hlist_del_init(&(hash_node->hash));
                return 1;
        }
        else
        {
                printf("cant find key in bucket\n");
                return -1;
        }
}

int hash_add(unsigned char key, unsigned char index)
{
        int j = hash_func(key);
        if(q_coef_list[j].coef == 0xff)
        {
                q_coef_list[j].coef = key;
                q_coef_list[j].index = index;
                hlist_add_head(&q_coef_list[j].hash, &hashtbl[j]);
                printf("add key to bucket %d,addr:%p\n",j, &q_coef_list[j]);
        }
        else
        {
                struct q_coef * dy_q_coef = (struct q_coef *)malloc(sizeof(struct q_coef));
                dy_q_coef->coef = key;
                dy_q_coef->index = index;
                INIT_HLIST_NODE(&(dy_q_coef->hash));
                printf("add key to bucket %d list, key=0x%02x, index=%d,addr:%p\n",j, key, index, (dy_q_coef));
                hlist_add_head(&(dy_q_coef->hash), &hashtbl[j]);

        }
        return 1;

}

哈希表完整代码

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值