数据结构-如何用二叉树实现hash表(C语言)

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构-如何用二叉树实现hash表(C语言)相关的知识,希望对你有一定的参考价值。

参考技术A       在用二叉树实现散列表之前,首先需要明白的是二叉树与散列表各自的特性。

       对于二叉树,主要表现形式是一对二;拿链型二叉树来说,即一个结点含有一个前驱,两个后继,两个后继即是该结点的两个孩子,如图。建立和处理用递归的方法可以轻松实现。具体关于树的结构在此不再详解。

       对于散列表,通俗来说,即对于将要插入的元素再通过一种函数式(如取余)后,得到一个具体数字(或字符),这个数字(或字符)将决定该元素所存放的位置,如果这个位置已经存有元素,则需要再次经过一系列的处理(或计算)得到一个新的地址位置,然后再原来的元素存入该为空的位置;具体的实现方法在此也不再详解。

       在对两者进行结合的过程中,需要考虑两者的特性,然后再进行实现。下面是个人在VC环境下使用了两个自定义函数对其进行中序实现的过程。

      数字可以分为两种,奇数和偶数,根据此特点,可以对该数字进行不断的除二取余的方式得到储存地址

      如图,首先插入的数是2,2%2==0,则在头结点的左边建立一个结点,存放数字2,并将其地址赋予头结点的lchild,然后插入的是奇数3,判断得其为奇数,故存放与右侧地址2;在再次插入偶数6时,经判断,6是偶数,而地址1中又存有元素,则需要重新寻找新的空的地址,再次做出的处理是6/2=3;3是奇数,则存放的是4,即作为1的右子树,依次类推,在相应的地址已经存放有元素的情况下,改变参数,寻找新的地址。由于每个数在取余过一定的次数以后,得到的数是不会重复的,所以每一个数字都会有一个地址来存放,不会发生冲突,二叉树就可以顺利实现。

      在其中需要注意的是,每个元素在经过除二之后虽然参数发生了变化,但是在找到并开辟好了需要存放该元素的空间后,存放的元素却还是客户端键入的原数字,所以在程序的运行中,要注意赋值对象是否正确。所以我在程序的实现中,采用了函数嵌套的方法,一个函数的作用是找到地址开辟空间,其返回值便是需要进行赋值的空间,在调用过后,只需将原来的元素放到该返回地址的相应区域即可。

       如果各位还不够明白,则下面开始解读程序。

       在运行开始,用户首先键入数字2,初始化成功的情况下,判断出2的存放位置应是位置1,(满足条件2%2==0&&p->lchild==NULL)这时建立一个结点,把这个结点放到头结点的左子树处,返回这个新建结点的地址,find函数执行一次,由于判断语句是else if型,直接退出进入insert函数实现赋值。在插入12时,满足else if (data%2==0&&p->lchild !=NULL),进入递归,p指向的是地址1,12/2==6,参数为6,对6继续判断,满足else if (data%2==0&&p->lchild!=NULL),再次递归,直到else if (data%2==1&&p->rchild==NULL),此时,data为3,开始创建空间并且退出递归,最后,在insert中返回的地址仍为新开辟的地址,此时开始赋值。

       在进行删除的过程中,只需要找到对应与欲删除元素相匹配的地址,然后改变地址中元素的值即可(如if (p->data==data)p->data=-1;)遍历时遇到-1即不进行遍历即可,不需要释放空间。

以上存属个人看法,如有不足,欢迎指正。

数据结构:如何用C语言快速实现带头双向循环链表

(又名:带哨兵位双向循环链表、带头结点的双向循环链表)

前言:

我们在之前学双向带头循环链表时,结尾部分简单讲解了快速实现的方法。本篇博客将详细讲解如何迅速实现,通过思路草图的方法轻松写出带头双向循环链表,甚至都可以直接用注释画草图。本篇博客是对 "从零开始逐步实现带哨兵位循环双向链表" 的补充,之前在写那篇博客的时候不小心忘记实现销毁接口了,这里正好能进行一个补充。

🔗 链接:【数据结构】从零开始逐步实现带哨兵位循环双向链表


一、 代码讲解

如果有人叫你快速实现一个链表,我们当然首选带头双向循环链表,因为他足够简单,虽然结构很复杂,但是就是因为结构复杂,反而让我们能够轻轻松松地实现。

我们只需要把插入和删除两个接口实现,就可以把链表的头插尾插头删尾删都搞定。直接复用插入和删除两个接口即可轻松搞定。再写一些链表常见的功能就可以大功告成了!

要实现的功能如下:

链表的初始化和销毁,链表的打印和查找,链表的插入和删除,头删尾插头插尾插。

0x00 定义链表

💬 创建 DList.h 文件:

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

typedef int DListNodeDataType;
typedef struct DoubleListNode 
    DListNodeDataType data;  // 用来存放结点的数据
    struct DoubleListNode* next;  // 指向后继节点的指针
    struct DoubleListNode* prev;  // 指向前驱节点的指针
 DLNode;  // 重命名为DLNode

DLNode* DLNodeInit();
void DLNodeDestroy(DLNode* pHead);
void DLNodePrint(DLNode* pHead);
DLNode* DLNodeFind(DLNode* pHead, DListNodeDataType x);

void DLNodeInsert(DLNode* pos, DListNodeDataType x);
void DLNodeDelete(DLNode* pos);

void DLNodePushBack(DLNode* pHead, DListNodeDataType x);
void DLNodePushFront(DLNode* pHead, DListNodeDataType x);
void DLNodePopBack(DLNode* pHead);
void DLNodePopFront(DLNode* pHead);

0x01 初始化(DLNodeInit)

💬 创建 DList.c 文件:

#include "DList.h"

DLNode* DLNodeInit() 
    DLNode* pHead = (DLNode*)malloc(sizeof(DLNode)); // 创建哨兵位头结点
    pHead->next = pHead->prev = pHead; // 让next和prev一开始都默认指向pHead
    return pHead; // 将phead作为结果返回

🔑 解读:这里我们使用 malloc 函数开辟一块空间作为 "哨兵位" pHead ,nextprev 此时都应该指向 pHead 。最后再将 pHead 作为结果返回回去,外面就可以接收到了。

0x02 销毁(DLNodeDestroy)

💬 DList.c

void DLNodeDestroy(DLNode* pHead) 
    assert(pHead); // 防止pHead为空
    DLNode* cursor = pHead->next; // 因为pHead存的不是有效数据,所以要从pHead的下一个结点开始
    while (cursor != pHead)   // 碰到哨兵位说明遍历完一遍了
        DLNode* next = cursor->next;  // 记录,防止free掉后cursor动不了
        free(cursor);  // 释放 cursor 当前指向的结点
        cursor = next;  // 让cursor继续往后走
    
    free(pHead); // 干掉掉哨兵
    pHead = NULL; // 置空防野指针

🔑 解读:

① 这里创建一个叫 cursor 的指针,用来遍历整个链表。pHead 存的不是有效数据,所以要从pHead->next 开始。

② 让 cursor 遍历每一个结点,直到碰到 pHead 结束,因为碰到 pHead 说明已经遍历完一遍了。

③ 进入循环后,创建一个 next 指针,提前记录一下 cursor->next ,可以有效阻止 free 掉之后 cursor 动不了的情况。释放完 cursor 后,再将 next 交给 cursor 就能让 cursor 继续往后走了。

④ 最后释放掉哨兵位,再把它置为空即可。

0x02 打印(DLNodePrint)

💬 DList.c

void DLNodePrint(DLNode* pHead) 
    assert(pHead);
    DLNode* cursor = pHead->next; // 因为pHead存的不是有效数据,所以要从pHead的下一个结点开始
    while (cursor != pHead)   // 碰到哨兵位说明遍历完一遍了
        printf("%d", cursor->data); // 打印 cursor 当前指向的节点的数据
        cursor = cursor->next; // 让cursor继续往后走
    
    printf("\\n"); // 换行

🔑 解读:创建 cursor 指针遍历整个链表来打印数据。比较简单,这里就不过多赘述了。

0x03 查找(DLNodeFind)

💬 DList.c

DLNode* DLNodeFind(DLNode* pHead, DListNodeDataType x) 
    assert(pHead != NULL);
    DLNode* cursor = pHead->next;
    while (cursor != NULL) 
        if (cursor->data == x) 
            return cursor;    
         else 
            cursor = cursor->next;
        
    
    return NULL;

🔑 解读:创建 cursor 指针,将 cursor->data x 比较即可。

0x04 插入(DLNodeInsert)

📚 默认为 pos 位置前插入,既然是插入,我们先写好创建新结点的接口:

创建新结点(CreateNewNode)

DLNode* CreateNewNode(DListNodeDataType x) 
    DLNode* new_node = (DLNode*)malloc(sizeof(DLNode)); // 创建新结点,给一块DLNode大小的空间
    if(new_node == NULL)   // 检查malloc
        printf("malloc failed!\\n");
        exit(-1);
    
    new_node->data = x;  // 放置数据
    new_node->next = new_node->prev = NULL;  // 默认置为空
    return new_node;

💬 DList.c

void DLNodeInsert(DLNode* pos, DListNodeDataType x) 
    // 思路草图: posPrev <->  new_node(待插目标) <->  pos
    assert(pos);
    DLNode* new_node = CreateNewNode(x); // 创建新节点
    DLNode* posPrev = pos->prev; // 创建pos的前驱指针
    // 把它们相互链接起来
    posPrev->next = new_node;
    new_node->prev = posPrev;
    new_node->next = pos;
    pos->prev = new_node;

🔑 解读:

① 首先创建新节点。因为是在 pos 位置之前插入。为了方便,我们先标出 pos 的前驱指针 posPrev,随后我们画出草图:

思路草图: posPrev <->  new_node(待插目标) <->  pos

这么一画,思路立马就清楚了,我们就可以轻轻松松把思路转换成代码:

② 将 posPrev 与 new_node 相互链接起来:

posPrev->next = new_node;
new_node->prev = posPrev;

③ 将 new_node 与 pos 相互链接起来:

new_node->next = pos;
pos->prev = new_node;

0x05 删除(DLNodeDelete)

📚 删除默认是删除 pos 位置的节点。

💬 DList.c

void DLNodeDelete(DLNode* pos) 
    // 思路草图:  posPrev   pos(待删目标)   posNext
    //               ↓_________________________↑
    assert(pos);
    DLNode* posPrev = pos->prev;
    DLNode* posNext = pos->next;
    // 把他们互相缝合起来
    posPrev->next = posNext; 
    posNext->prev = posPrev;
    // 删除pos位置的结点(释放并置空)
    free(pos);
    pos = NULL;

① 既然要删除 pos 位置的节点,我们先标出 pos 的前驱指针 posPrev pos 的后继指针 posNext ,随后我们画出草图:

    思路草图:  posPrev   pos(待删目标)   posNext

                             ↓_______________ ______↑

因为 pos 要被删掉了,我们需要把 posPrev posNext 位置的两个节点互相缝合起来:

posPrev->next = posNext; 
posNext->prev = posPrev;

② 最后删除 pos 位置的结点,释放并置空。

0x06 尾插头插 / 尾删头删

📚 插入和删除都写好了,头插尾插和头删尾删直接复用就能轻松解决!

💬 DList.c

尾插(DLNodePushBack)

void DLNodePushBack(DLNode* pHead, DListNodeDataType x) 
    // 思路草图:  pHead   ...   end  (x)
    //            posPrev(end) <->  new_node(待插目标) <->  pos(pHead)
    assert(pHead);
    DLNodeInsert(pHead, x);

🔑 解读:直接调用插入接口即可。因为尾插要找到最后一个结点,所以我们传入 pHead 让它作为 pos,这样就能找到 posPrevposPrev 就是最后一个节点。思路草图如下:

pHead   ...   end  (x)

posPrev(end) <->  new_node(待插目标) <->  pos(pHead)

头插(DLNodePushFront)

void DLNodePushFront(DLNode* pHead, DListNodeDataType x) 
    // 思路草图:  pHead     pHeadNext
    //            posPrev(pHead) <->  new_node(待插目标) <->  pos(pHeadNext)
    assert(pHead);
    DLNodeInsert(pHead->next, x);

🔑 解读:因为头结点是哨兵位,存得不是有效数据,所以它的头插要在哨兵位后面(即第一个有效数据前)。思路草图如下:

pHead     pHeadNext

posPrev(pHead) <->  new_node(待插目标) <->  pos(pHeadNext)

尾删(DLNodePopBack)

void DLNodePopBack(DLNode* pHead) 
    // 思路草图: pHead ... pos
    // 思路草图:  posPrev(endPrev)   pos(end / 待删目标)   posNext(pHead)
    //               ↓__________________________________________↑
    assert(pHead);
    DLNodeDelete(pHead->prev);

🔑 解读:尾删,默认删除的是 pos 位置的结点。所以我们直接传入要删除的结点即可。找到最后一个结点很简单,直接 pHead->prev 就是最后一个节点了,这就是结构的优势(而不像普通单链表要遍历链表找到尾指针)。思路草图如下:

pHead ... pos

posPrev(endPrev)   pos(end / 待删目标)   posNext(pHead)

   ↓_____________________________________↑

头删(DLNodePopFront)

void DLNodePopFront(DLNode* pHead) 
    // 思路草图: pHead  pos
    // 思路草图:  posPrev(pHead)   pos(pHead->next / 待删目标)   posNext(pHead->next->next)
    //               ↓_______________________________________________↑
    assert(pHead);
    DLNodeDelete(pHead->next);

🔑 解读:头删,删除第一个有效节点,传入 pHead->next 即可。思路草图如下:

pHead  pos

posPrev(pHead)   pos(pHead->next / 待删目标)   posNext(pHead->next->next)

     ↓________________________________________↑

二、完整代码

💬 DList.h

#include <stdio.h>
#include <stdlib.h>
#include <assert.h>

typedef int DListNodeDataType;
typedef struct DoubleListNode 
    DListNodeDataType data;  // 用来存放结点的数据
    struct DoubleListNode* next;  // 指向后继节点的指针
    struct DoubleListNode* prev;  // 指向前驱节点的指针
 DLNode;  // 重命名为DLNode

DLNode* DLNodeInit();
void DLNodeDestroy(DLNode* pHead);
void DLNodePrint(DLNode* pHead);
DLNode* DLNodeFind(DLNode* pHead, DListNodeDataType x);

void DLNodeInsert(DLNode* pos, DListNodeDataType x);
void DLNodeDelete(DLNode* pos);

void DLNodePushBack(DLNode* pHead, DListNodeDataType x);
void DLNodePushFront(DLNode* pHead, DListNodeDataType x);
void DLNodePopBack(DLNode* pHead);
void DLNodePopFront(DLNode* pHead);

💬 DList.c

#include "DList.h"

DLNode* DLNodeInit() 
    DLNode* pHead = (DLNode*)malloc(sizeof(DLNode)); // 创建哨兵位头结点
    pHead->next = pHead->prev = pHead; // 让next和prev一开始都默认指向pHead
    return pHead; // 将phead作为结果返回


void DLNodeDestroy(DLNode* pHead) 
    assert(pHead); // 防止pHead为空
    DLNode* cursor = pHead->next; // 因为pHead存的不是有效数据,所以要从pHead的下一个结点开始
    while (cursor != pHead)   // 碰到哨兵位说明遍历完一遍了
        DLNode* next = cursor->next;  // 记录,防止free掉后cursor动不了
        free(cursor);  // 释放 cursor 当前指向的结点
        cursor = next;  // 让cursor继续往后走
    
    free(pHead); // 干掉掉哨兵
    pHead = NULL; // 置空防野指针


void DLNodePrint(DLNode* pHead) 
    assert(pHead);
    DLNode* cursor = pHead->next; // 因为pHead存的不是有效数据,所以要从pHead的下一个结点开始
    while (cursor != pHead)   // 碰到哨兵位说明遍历完一遍了
        printf("%d", cursor->data); // 打印 cursor 当前指向的节点的数据
        cursor = cursor->next; // 让cursor继续往后走
    
    printf("\\n"); // 换行


DLNode* DLNodeFind(DLNode* pHead, DListNodeDataType x) 
    assert(pHead != NULL);
    DLNode* cursor = pHead->next;
    while (cursor != NULL) 
        if (cursor->data == x) 
            return cursor;    
         else 
            cursor = cursor->next;
        
    
    return NULL;


DLNode* CreateNewNode(DListNodeDataType x) 
    DLNode* new_node = (DLNode*)malloc(sizeof(DLNode)); // 创建新结点,给一块DLNode大小的空间
    if(new_node == NULL)   // 检查malloc
        printf("malloc failed!\\n");
        exit(-1);
    
    new_node->data = x;  // 放置数据
    new_node->next = new_node->prev = NULL;  // 默认置为空
    return new_node;


void DLNodeInsert(DLNode* pos, DListNodeDataType x) 
    // 思路草图: posPrev <->  new_node(待插目标) <->  pos
    assert(pos);
    DLNode* new_node = CreateNewNode(x); // 创建新节点
    DLNode* posPrev = pos->prev; // 创建pos的前驱指针
    // 把它们相互链接起来
    posPrev->next = new_node;
    new_node->prev = posPrev;
    new_node->next = pos;
    pos->prev = new_node;


void DLNodeDelete(DLNode* pos) 
    // 思路草图:  posPrev   pos(待删目标)   posNext
    //               ↓_________________________↑
    assert(pos);
    DLNode* posPrev = pos->prev;
    DLNode* posNext = pos->next;
    // 把他们互相缝合起来
    posPrev->next = posNext; 
    posNext->prev = posPrev;
    // 删除pos位置的结点(释放并置空)
    free(pos);
    pos = NULL;


void DLNodePushBack(DLNode* pHead, DListNodeDataType x) 
    // 思路草图:  pHead   ...   end  (x)
    //            posPrev(end) <->  new_node(待插目标) <->  pos(pHead)
    assert(pHead);
    DLNodeInsert(pHead, x);


void DLNodePushFront(DLNode* pHead, DListNodeDataType x) 
    // 思路草图:  pHead     pHeadNext
    //            posPrev(pHead) <->  new_node(待插目标) <->  pos(pHeadNext)
    assert(pHead);
    DLNodeInsert(pHead->next, x);


void DLNodePopBack(DLNode* pHead) 
    // 思路草图: pHead ... pos
    // 思路草图:  posPrev(endPrev)   pos(end / 待删目标)   posNext(pHead)
    //               ↓__________________________________________↑
    assert(pHead);
    DLNodeDelete(pHead->prev);


void DLNodePopFront(DLNode* pHead) 
    // 思路草图: pHead  pos
    // 思路草图:  posPrev(pHead)   pos(pHead->next / 待删目标)   posNext(pHead->next->next)
    //               ↓_______________________________________________↑
    assert(pHead);
    DLNodeDelete(pHead->next);


参考资料:

Microsoft. MSDN(Microsoft Developer Network)[EB/OL]. []. .

百度百科[EB/OL]. []. https://baike.baidu.com/.

声明:本章的 DoubleListNode 源代码参考自C++的STL库

📌 笔者:王亦优

📃 更新: 2021.12.9

❌ 勘误: 无

📜 声明: 由于作者水平有限,本文有错误和不准确之处在所难免,本人也很想知道这些错误,恳望读者批评指正!

以上是关于数据结构-如何用二叉树实现hash表(C语言)的主要内容,如果未能解决你的问题,请参考以下文章

用C语言编写程序,创建一个二叉树的二叉链表结构,然后输出从根结点到所有叶子结点的路径。

如何用 JS 实现二叉堆

如何用表(html)表示二叉树?

二叉树的基本操作 C语言版的

数据结构C语言二叉树

请问如何用随机函数生成二叉树,并遍历?