数据结构模版----单链表实现方式总结

来源:互联网 发布:中国美术出版总社 知乎 编辑:程序博客网 时间:2024/06/16 11:08

数据结构模版----单链表实现方式总结

前面我们提供了四种方式实现的单链表,有带头结点的不带头结点的,而单链表的结构体定义也有两种方式,那么这些实现方式,到底有什么区别呢,为什么会出现这么多种实现方式呢,下面我们就来细细体会

一 单链表结构体的实现区别

首先我们对比一下,单链表结构体

不同方式的单链表实现时,链表结点的实现是相同的,不同之处在于单链表结构体的实现上

单链表结构体的实现

typedef int ElemType;// 自定义数据类型//typedef struct LinkListNode*PLinkListNode;// 链表结点指针域// 链表结点数据域typedef struct LinkListNode{ElemTypem_data;// 数据域struct LinkListNode*m_next;// 指针域}LinkListNode;

①带length标识的单链表结构体

typedef struct LinkList{LinkListNode*m_head;// 链表头结点intm_length;// 单链表数据结点个数指针域}LinkList;

①不带length标识的单链表结构体

// 带头结点的单链表typedef struct LinkListNode LinkList;

// 不带头结点的单项链表typedef struct LinkListNode*  LinkList;

区别

    对于一个单链表来说,查找其长度的时,需要从前往后遍历一遍单链表,因此时间复杂度是O(N),为了能更快的获取其长度,我们设计了带length标识的单链表结构体,这对于经常获取链表长度时,效率是十分客观的,

而我们又发现同样时不带length标识的单链表结构体,定义的方式却又出现了细微的差别。这个又是为什么呢?、

这个区别的根本就在于就是带头结点的单链表和不带头结点的单链表的区别了。

单链表的结点插入和删除,其实就是修改其前驱结点的next指针域的指向,我们学C语言的时候知道,无论什么时候在函数中修改一个变量的值,需要传入这个变量的指针作为参数,具体的参见下面的示例程序
#include <stdio.h>#include <stdlib.h>// 函数中修改一个变量的值void Modify1(int value);// 变量作为参数void Modify2(int *value);// 变量指针作为参数int main(void){int value = 0;Modify1(value);printf("value = %d after func\n\n", value);// modify failedModify2(&value);printf("value = %d after func\n\n", value);// modify successreturn EXIT_SUCCESS;}// 变量作为参数void Modify1(int value){value = 10;printf("value = %d in %s\n", value, __func__);}// 变量指针作为参数void Modify2(int *value){*value = 10;printf("value = %d in %s\n", *value, __func__);}
那么单链表中删除删除元素时,需要修改在函数中修改指针的指向(尤其在插入首元时,需要修改头指针的指向),因此需要使用指针的指针(即二重指针)
为了保证我们实现的代码的一致性,我们所有参数的传递均使用LinkList *list,作为参数传递到函数中
这样在带length标识的单链表结构体中,修改指针的指向时其实使用的已经是
LinkListNode *pNode = list->m_head;// 传入的是Linlist *, 而修改的是*list中的LinkListNode *m_head可见已经是二重指针
那么是作为二重指针,是可以修改掉m_head的指向的(我们可可以这样理解,传入的是LinkList* list即变量的指针,那么毕竟可以修改掉变量*list的值,变量*list作为结构体,他的数据就是*m_head和m_length,即可以修改掉指针的指向)
#include <stdio.h>#include <stdlib.h>// 函数中修改一个变量的值typedef struct{int *value;int length}List;int gloValue1 = 0;int gloValue2 = 10;// 全局变量void Modify1(List list);// 变量作为参数void Modify2(List *list);// 变量的指针作为参数int main(void){List list = {&gloValue1, 0};printf("&gloValue1 = %p, &gloValue = %p\n", &gloValue1, &gloValue2);printf("value = %d, addr = %p, length = %d after func\n\n", *(list.value), list.value, list.length);Modify1(list);printf("value = %d, addr = %p, length = %d after Modify1\n\n", *(list.value), list.value, list.length);Modify2(&list);printf("value = %d, addr = %p, length = %d after Modify2\n\n", *(list.value), list.value, list.length);return EXIT_SUCCESS;}// 变量作为参数void Modify1(List list){list.value = &gloValue2;// 修改指针的值(即指针的指向)  failedlist.length = 10;// failedprintf("value = %d, addr = %p, length = %d in %s\n", *(list.value), list.value, list.length, __func__);}// 变量作为参数void Modify2(List *list){list->value = &gloValue2;// 修改指针的值(即指针的指向) successlist->length = 10;//  successprintf("value = %d, addr = %p, length = %d in %s\n", *(list->value), list->value, list->length, __func__);}
    那么现在问题就清楚了,要想修改一个指针的指向,需要传入指针的指针作为参数,那么我们在插入删除的过程中,在那里修改了指针的指向呢?
    其实归根结底修改了两个地方,我们参见不带头结点的单链表实现方式2中插入函数的实现
①是修改结点本身的指向,需要传入二重指针
// 不带头结点的单链表插入首元时,需要修改头指针的指向(*list) = newNode;// 此时修改指针本身的指向需要传入LinkListNode **// 不带头结点的单项链表声明typedef struct LinkListNode*  LinkList;//函数声明LinkListNode* InsertNode(LinkList *list,// LinkList * == LinkListNode**  int position,  ElemType data)

②是修改结点的指针域的指向,传入指向结点的指针即可
// 将newNode插入pNode之后时,需要修改newNode和pNode指针域的指向newNode->m_next = pNode->m_next;// 此时只需要传入LinkListNode *即可, 因为指针域m_next是node的数据成员, 已经是个指针成员变量pNode->m_next = newNode;// 传入LinkListNode *, m_next就成为一个指针的指针// 函数声明LinkListNode *AddNode(LinkList *list,  LinkListNode *prevNode,// 传入LinkListNode *, 即可修改prevNode->m_next = @@@@@  ElemType data)
现在问题明了了,但是为什么两种方式定义的单链表结构体却不一样呢
// 不带头结点的单项链表typedef struct LinkListNode*  LinkList;// 带头结点的单链表typedef struct LinkListNode LinkList;
这就是不带头结点的单链表和带头结点的单链表实现地方的区别了

二 不带头结点的单链表和带头结点的单链表的区别

我们很容易发现带头结点的单链表实现起来要比不带头结点的单链表要简单,这个是为什么呢
继续接着上面的讲,插入和删除时,要修改其前驱指针的指向,特别的插入和删除第一个元素(即首元结点)时,我们需要更改头指针的指向,很明显头指针是没有前驱的,这样我们实现起来的时候
对于非头结点,我们直接找到其前驱,然后修改其前驱结点的指针域的指向就好了(传入指向结点的指针即可)
对于头结点,没有前驱结点,我们需要特殊判断,而直接修改头指针本身的指向(传入指向头结点的指针的指针)
怎么规避这个问题呢,那就在链表头的位置添加一个头结点,初始化时候将头结点创建好,这样我们即使插入首元结点,首元结点也是有一个前驱(即头结点的),这样我们修改头指针,也就变成了修改头结点的指针域的指向,这样我们传参和插入删除的实现就统一了。实现起来也简单
好了不带头结点和带头结点我么明了了,接着来回答上面那个问题,为什么,单链表的结构体不一样呢。。
带头结点的时候,我们传入参数其实已经不需要传入二重指针了,统一传入指向链表结点的指针即可,因此用下面的实现即可
// 带头结点的单链表typedef struct LinkListNode LinkList;
而不带头结点的时候,实现起来插入删除时候,是需要传入指向头结点的指针的指针,那么还用LinkListNode作为List,传入参数时,就需要List **list了,这样我们几种实现方式的函数定义就不统一了,这种编码方式不是我们喜欢的,因此用
// 不带头结点的单项链表typedef struct LinkListNode*  LinkList;
无独有偶,上面带头节点的单链表也是可以用typedef struct LinkListNode*  LinkList;,只是传参时候用List list即可,函数命名也不统一了,而若是将参数统一为List *list,那么里面代码实现的时候,全部用(*list)表示指向头结点的指针,实现起来不容易理解,因此我们选用了typedef struct LinkListNode  LinkList


0 0
原创粉丝点击