双向带头循环链表的实现以及动态顺序表和单链表的对比

双向循环链表对比单链表,他的实现会更简单,不需要考虑太多

双向带头循环链表的结构体声明

typedef int DLTDataType;
typedef struct DListNode
{
    //节点里面包含两个指针,一个是指向他上一个节点的指针prev
    //另外一个就是指向他下一个节点的指针next
    struct DListNode* prev;
    struct DListNode* next;
    DLTDataType data;
}DLTNode;

首先我们需要先初始化

DLTNode* DListNodeInit(void)
{
    //初始化创建一个头节点
    DLTNode* guard = (DLTNode*)malloc(sizeof(DLTNode));
    if(guard == NULL)
    {
        perror("malloc fail");
        exit(-1);
    }
    //这个头节点的prev和next都指向他自己
    guard->next = guard;
    guard->prev = guard;
    //最后我们以返回的形式,传回去头节点head
    return guard;
}

接下来是打印这个链表

void DListNodePrint(DLTNode* phead)
{
    assert(phead);
    //由于我们的头节点不保存数据,所以我们从头节点的下一个节点开始打印数据
    DLTNode* cur=phead->next;
    printf("phead<=>");
    //对于双向带头循环链表的判断停止,不是判断null了,由于是循环的,他的尾会跟头是连着的,所以判断                //结束的是,cur 不等于头就代表这个链表都访问结束了
    while(cur != phead)
    {
        printf("%d<=>",cur->data);
        cur = cur->next;
    }
    printf("
");
}

由于每次数据的插入都需要创建一个节点,所以我们创建一个函数,用来创建这个节点、

DLTNode* BuyDListNode(DLTDataType x)
{
    DLTNode* node = (DLTNode*)malloc(sizeof(DLTNode));
    if(node == NULL)
    {
        perror("BuyDListNode fail");
        exit(-1);
    }
    //每个新节点的头指向自己,尾也是指向自己
    node->data = x;
    node->next = node;
    node->prev =node;
    return node;
}

数据的尾插

void DListNodePushBack(DLTNode* phead,DLTDataType x)
{
      assert(phead);

      DLTNode* newnode = BuyDListNode(x);
      DLTNode* tail = phead->prev;
      //接下来是节点之间的指向关系了,由于我们有尾节点和头节点,所以不用考虑指向的先后顺序
      tail->next = newnode;
      newnode->prev = tail;
      phead->prev = newnode;
      newnode->next = phead;
      //这个是在pos的位置前插入一个节点的函数接口,在下面会介绍到
      //DListNodeInsert(phead,x);

}

数据的头插

void DListNodePushFront(DLTNode* phead,DLTDataType x)
{
    assert(phead);
//    DLTNode* newnode=BuyDListNode(x);
    //第一种头插,需要考虑连接顺序
//    phead->next->prev = newnode;
//    newnode->prev = phead;
//    newnode->next = phead->next;
//    phead->next = newnode;
    //第二种头插,我们有了头节点和下一节点的地址了,不需要考虑连接顺序
//    DLTNode* first = phead->next;
//    phead->next = newnode;
//    newnode->prev = phead;
//    first->prev = newnode;
//    newnode->next = first;
    //这个是在pos之前插入一个节点的函数接口,下面会有介绍
    //DListNodeInsert(phead->next,x);
}

数据的尾删

void DListNodePopBack(DLTNode* phead)
{
    assert(phead);
    //在删除之前,我们需要先判断一下,这个链表是否为空,为空就没有必要删了
    assert(!DListEmpty(phead));
    //下面是删除的指向步骤
    DLTNode* del = phead->prev;
    DLTNode* prev = del->prev;
    prev->next = phead;
    phead->prev = prev;
    free(del);
    del = NULL;
}

数据的头删

void DListNodePopFront(DLTNode* phead)
{
    assert(phead);
    //数据的头删跟尾删是类似的,也是要先判断这个链表是否为空
    assert(!DListEmpty(phead));
    DLTNode* del=phead->next;
    DLTNode* next = del->next;
    next->prev = phead;
    phead->next = next;
    free(del);
    del = NULL;
}

查找数据所在的节点地址,如果找到了返回这个节点的地址,如果没有找到就返回null

DLTNode* FindDListNode(DLTNode* phead,DLTDataType x)
{
    assert(phead);
    DLTNode* cur = phead->next;
    while(cur != phead)
    {
        if(cur->data == x)
        {
            return cur;
        }
        cur = cur->next;
    }
    return NULL;
}

接下来是可以代替头插,尾插的函数,在指定的地址前插入数据,有了这个函数,我们可以不用写头插,尾插,头插和尾插的函数接口可以改为      这是尾插 DListNodeInsert(phead,x);

这是头插     DListNodeInsert(phead->next,x); 直接放在对应的函数接口里面就行,原先的头插尾插代码都可以注释掉

void DListNodeInsert(DLTNode* pos, DLTDataType x)
{
    assert(pos);
    DLTNode* prev = pos->prev;
    DLTNode* newnode = BuyDListNode(x);
    
    prev->next = newnode;
    newnode->prev = prev;
    newnode->next = pos;
    pos->prev = newnode;
}

当然,有了在pos前插入数据,也会有在pos之前删除数据的

void DListNodeErase(DLTNode* pos)
{
    assert(pos);
    DLTNode* prev = pos->prev;
    DLTNode* next = pos->next;
    prev->next = next;
    next->prev = prev;
    free(pos);
    //由于不是用二级指针,这里,无法更改这个指针指向的地址置空
//    pos = NULL;
}

接下来是最后一个函数接口,就是销毁这个链表

void DListNodeDestory(DLTNode* phead)
{
    assert(phead);
    DLTNode* cur = phead;
    while(cur != phead)
    {
        DLTNode* next = cur->next;
        free(cur);
        cur=next;
        
    }
    free(phead);
}

36863ca6417a42ab9f6deb050bcd8fcc.png

 

到此就是一个双向带头循环链表接口;

接下来的是动态顺序表和单链表之间对比的优势和劣势;

顺序表:他在物理上的结构就是一窜连续的地址,我们可以通过下标对其进行随机访问,尾插是O(1)的,头插就差一些是O(n)的,这是一个动态的顺序表,他是可以扩容的,用realloc进行扩容的时候,会有两种情况,一种是这个顺序表后面刚好有他需要扩容的空间,一种是没有

如果有直接把所需要的空间给到顺序表进行扩容,如果没有就需要在内存的其他空间进行寻找,把数据都搬过去,所以异地扩容就比较消耗时间了,相对的数据的存储是连续的,也是有好处的,我们的CPU在读取数据之前都会在寄存器上读取,寄存器又在我们的内存上拿数据,他拿数据不是只拿一个的,他会把这个数据的周边几个数据一起拿走,至于拿几个,这个要看型号,数据存储在连续的地址,会提高寄存器的命中率,减少寄存器多次到内存取数据;

单链表:单链表是按需创建空间的,不存在浪费,但他每次都需要重新创建空间,所以跟顺序表对比上,并没有多少优势,就是用空间换取时间,在现在这个社会内存没有时间重要了,单链表的头插是O(1)的,尾插是O(n)的,刚好跟顺序表是相反的,对于具体选择单链表好,还是顺序表,要看使用场景了,如果知道大概的数据大小,那无疑用顺序表是比较好的,如果数据是需要频繁的插入和删除,那单链表会比顺序表好。

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值