线性链式表
2016-01-18 12:05
253 查看
定义
线性链式表就是我们常说的链表,也被称为线性表的非顺序映像或链式映像。它与线性顺序表的最大区别就是各个存储单元是非连续的。它无法通过数学公式来确定每个存储单元在内存中的关系。链表中最重要的两个概念就是结点(Node)和next指针。在单向链表中结点通常只包含两个存储域,一个是data数据域,用来存储结点所代表的值。第二个是,next指针域,用来存储结点的后继。因此next指针成了寻找结点后继的唯一途径,也就是只有知道头结点,才能通过next指针一个个往下寻找。所以链表的结构是链式的,而非顺序的,他们在内存结构中往往是离散的。分类
按照结点中指针域的个数分类有两种: 单向链表和双向链表单向链表就是一个结点只包含一个next指针域,即一个结点只知道它的后继结点是谁。遍历链表的时候,只能从前往后遍历。
双向链表就是一个结点包含pre指针域和next指针域,即一个结点即知道它的后继结点,也知道它的前驱结点。遍历链表的时候,可以从前往后,也可以从后往前。但是从后往前。从前往后需要head指针开始,从后往前需要tail指针开始。
两者区别: 结点的前驱和后继是否都知道,遍历的时候是否支持双向遍历。
按照存储方式分类有两种: 动态链表和静态链表
动态链表就是平常我们最常见的链表,链表的插入,删除,建立都是动态执行的。没有内存空间的预申请以及浪费。通常结点的定义方式就是包含data和next指针的结构体。当需要新建结点插入的时候通常用malloc申请新的结点内存空间,进行初始化数据域,修改指针域插入链表。当需要删除结点的时候,往往先修改链表的指针域,然后free结点。因此动态链表的元素容量不是固定的,是根据需求变化的。
静态链表是给有些不支持指针的编程语言实现链表的一种方式,通常用数组来实现。数组的每个分量是一个复合类型数据(结构体或者说类Class)。每个分量都有两个存储域,一个是data数据域,第二个next数据域,是保存当前分量的后继结点位于数组中的第几个分量。通常静态量插入,删除,建立都需要一个备用空间链表来管理内存空间的复用。插入的时候,需要从备用空间链表中申请复用可以复用的结点。如果备用空间链表没有可以复用的结点,往往把数组第一个没有存储任何数据的分量作为下一个结点使用。删除的时候,要将删除的结点放入备用空间链表,以提供复用。静态链表的容量是预申请的,是固定的。
两者区别: 动态链表存储空间不是预申请,容量不固定。静态链表存储空间是预申请的,容量通常在申请数组大小的就确定下来了。
按照末尾结点指针域是否为头结点分类: 循环链表和非循环链表
循环链表 即链表最后一个结点的next指针域为头结点,构成环状结构。通常遍历链表的结束状态谓词不能用结点!=NULL来确定,而是应该用结点的后继结点!=HEAD结点。
非循环链表 即链表最后一个结点的next指针域为NULL,不构成环状结构。遍历链表的结束状态谓词用结点!=NULL来确定。
两者区别: 末尾结点的next指针域是否指向头结点。
相关操作以及时间复杂度分析
索引(Index)
由于链表每个存储单元不是连续的,也没有任何数学公式可以表示,因此索引操作只能通过遍历来获取。遍历通常用next指针域来找到对应结点, 假设索引链表的第5个数据,通常要从头结点开始,经过for(int i=0; i<5; i++) p = p->next,到达第五个结点,然后返回对应的值(
return p->data;)。现在假设i结点被索引的概率为pi。则索引操作的数学期望值为:
现在假设pi都具有相同的概率1/n; 那么对上述表达式进行化简得到如下结果:
再将如上级数进行整理可得
因此索引的时间复杂度为O(n)。
查找(find)
因为在链表中查找也是需要通过遍历的, 它在遍历的过程和索引操作是一样的,只不过循环的终止条件是是否查找到对应的值,而非需要走到位置的次数。现在假设需要寻找的值在结点i的概率为pi。则查找操作的数学期望值为:现在假设pi都具有相同的概率1/n; 那么对上述表达式进行化简得到如下结果:
再将如上级数进行整理可得
因此查找的时间复杂度为O(n)。
插入(Insert)
与顺序表相比,链表的最大特点就在于插入与删除的方便。它不需要将插入位置之后的元素全部往后移动一个位置,只需要将插入结点的next指针域指向待插入位置之后的结点,将待插入位置之前的结点的next指针域指向插入结点就完成了插入。例如,以下链表 A->B->C中,将D插入B,C之间,只要进行如下操作:D->next = C, B->next = D, 就完成了插入,形成了A->B->D->C。因此已经定位到插入位置的前提下链表插入的时间复杂度为O(1); 但是只给定在插入到第2个元素之前,还需要进行定位操作,定位操作就是索引操作的,因此时间复杂度是O(n)。
所以,如果没有定位到插入位置,链表插入操作的时间复杂度还是O(n)。
删除(Delete)
删除操作也是链表的一个优势,它不需要将删除位置以后的所有元素往前移动一个位置,只需要将删除位置之前的结点的next指针域指向删除位置之后的结点,然后free掉删除的结点即可。例如,以下链表 A->B->C中,将B删除,只要进行如下操作:A->next = C; free(B);就完成了删除,形成了A->C。因此已经定位到删除位置的前提下链表删除的时间复杂度为O(1); 但是只给定在删除第2个元素,还需要进行定位操作,定位操作就是索引操作的,因此时间复杂度是O(n)。
所以,如果没有定位删除位置,链表删除操作的时间复杂度还是O(n)。
合并(Merge)
链表合并操作的算法和线性顺序表的合并操作的算法是一样的,时间复杂度也是一样的,都是O(m+n)。只不过区别于线性顺序表的合并,链表不需要申请存放合并之后线性表的空间,而是只要修改各个节点next指针域,使得通过结点“打散-重组”的方式进行合并,因此空间复杂度明显比线性表要低。相关的合并算法见我的另一篇博文《线性顺序表》实现代码
动态单向链表
#include<stdio.h> #include<stdlib.h> typedef struct LNode *PNode; typedef struct LNode{ int data; PNode next; }LNode, *PNode; /* * 从后往前建立链表 */ void create_list(PNode *head, int n) { *head = (PNode) malloc(sizeof(LNode)); (*head)->data = n; (*head)->next = NULL; for(int i=n; i>0; i--){ PNode node = (PNode) malloc(sizeof(LNode)); scanf("%d", &node->data); node->next = (*head)->next; (*head)->next = node; } } /* * 在对应下标位置插入新的结点 */ void insert(PNode head, int index, int newValue) { PNode p = head; for(int i=0; i< index && p; i++) p = p->next; PNode newNode = (PNode)malloc(sizeof(LNode)); newNode->data = newValue; newNode->next = p->next; p->next = newNode; head->data++; } /* * 删除对应下标的链表结点 */ int deleteElement(PNode head, int index) { PNode p = head; for(int i=0; i < index && p; i++) p = p->next; PNode deleteNode = p->next; p->next = deleteNode->next; int deleteValue = deleteNode->data; free(deleteNode); // 释放结点内存 head->data--; return deleteValue; } /* * 获取对应下标元素值 */ int getNodeValue(PNode head, int index) { PNode p = head; for(int i=0; i <= index; i++) p = p->next; return p->data; } /* * 合并链表 */ void merge_list(PNode head1, PNode head2, PNode *rs) { PNode p1 = head1->next; PNode p2 = head2->next; *rs = (PNode) malloc(sizeof(LNode)); (*rs)->next = NULL; (*rs)->data = head1->data + head2->data; PNode prs = (*rs); while(p1&&p2){ if(p1->data <= p2->data){ prs->next = p1; p1 = p1->next; prs = prs->next; }else{ prs->next = p2; p2 = p2->next; prs = prs->next; } } while(p1) { prs->next = p1; p1 = p1->next; prs = prs->next; } while(p2) { prs->next = p2; p2 = p2->next; prs = prs->next; } free(head1); free(head2); } /* * 打印链表信息,包含链表长度,元素内容 */ void print_list_info(PNode head) { PNode p = head; printf("The length of linked list is %d\nData: ", head->data); while(p->next) { p = p->next; printf("%d ", p->data); } printf("\n"); } int main() { PNode head; create_list(&head, 10); print_list_info(head); insert(head, 0, 100); print_list_info(head); PNode head1; PNode head2; PNode rs; create_list(&head1, 5); create_list(&head2, 3); merge_list(head1, head2, &rs); print_list_info(rs); return 0; }
静态单向链表
#include<stdio.h> #include<stdlib.h> #define MAX_SIZE 100 // 链表的最大长度 #define END -1 typedef struct { int data; int next; } SLNode, SLinkedList[MAX_SIZE]; void create_slist(SLinkedList slist, int n) { slist[0].data = n; // 头结点保存链表长度信息 slist[0].next = 1; // 注意这里是从1开始的,不是0. 0用来作为链表的头结点了 for(int i=1; i<=n; i++){ scanf("%d", &slist[i].data); slist[i].next = i+1; } slist .next = END; // 链表最后一个结点的next为END(-1) } void init_backup_list(SLinkedList backupList) { backupList[0].data = 0; backupList[0].next = END; } /* * 从备用空间链表中重用结点空间 */ int malloc_node(SLinkedList backupList) { int m_node = backupList[0].next; // 备用空间链表的第一个结点 if(m_node != END) backupList[0].next = backupList[m_node].next; // 如果m_node不为END,也就是说备用空间链表的第一个结点可以被重用,则删除第一个结点,提供重用 return m_node; } /* * 将删除的结点移入备用空间链表中 */ void free_node(SLinkedList backupList, int index) { backupList[index].next = backupList[0].next; backupList[0].next = index; } void insert(SLinkedList slist, SLinkedList backupList, int index, int value) { int p = 0; for(int i=0; i < index; i++){ p = slist[p].next; } // 先从备用链表中查看是否有可以重用的结点 int insert_p = malloc_node(backupList); insert_p = insert_p == END ? slist[0].data+1 : insert_p; // 若insert_p为END说明没有重用的结点,则从链表空间中取下一个新的结点作为插入结点 // 插入结点 slist[insert_p].data = value; slist[insert_p].next = slist[p].next; slist[p].next = insert_p; slist[0].data++; } void deleteElement(SLinkedList slist, SLinkedList backupList, int index) { int p = 0; for(int i=0; i < index; i++){ p = slist[p].next; } slist[p].next = slist[slist[p].next].next; free_node(backupList, index); } int getElement(SLinkedList slist, int index) { int p = 0; for(int i=0; i <= index; i++){ p = slist[p].next; } return slist[p].data; } void print_slit_info(SLinkedList slist) { printf("The length of the static linked list is %d\nData: ", slist[0].data); int p = slist[0].next; while(p!=END){ printf("%d ", slist[p].data); p = slist[p].next; } printf("\n"); } int main() { SLinkedList slist; SLinkedList backupList; init_backup_list(backupList); create_slist(slist, 5); print_slit_info(slist); insert(slist, backupList, 2, 10); print_slit_info(slist); deleteElement(slist, backupList, 2); print_slit_info(slist); return 0; }
动态双向链表
#include<stdio.h> #include<stdlib.h> typedef struct DLNode *PDLNode; typedef struct DLNode{ int data; PDLNode pre; PDLNode next; }DLNode, *PDLNode; void create_list(PDLNode *head, PDLNode *tail, int n) { *head = (PDLNode) malloc(sizeof(DLNode)); *tail = (PDLNode) malloc(sizeof(DLNode)); (*head)->pre = NULL; (*head)->next = (*tail); (*head)->data = n; (*tail)->pre = (*head); (*tail)->next = NULL; (*tail)->data = n; for(int i=0; i<n; i++){ PDLNode node = (PDLNode) malloc(sizeof(DLNode)); scanf("%d", &node->data); node->pre = (*tail)->pre; (*tail)->pre->next = node; node->next = (*tail); (*tail)->pre = node; } } void insert(PDLNode head, int index, int value) { PDLNode p = head; for(int i=0; i<index; i++) p = p->next; PDLNode node = (PDLNode)malloc(sizeof(DLNode)); node->data = value; // 插入结点 node->pre = p; p->next->pre = node; node->next = p->next; p->next = node; } void deleteElement(PDLNode head, int index) { PDLNode p = head; for(int i=0; i<index; i++) p = p->next; PDLNode deleteNode = p->next; deleteNode->next->pre = p; p->next = deleteNode->next; free(deleteNode); } int getElement(PDLNode head, int index) { PDLNode p = head->next; for(int i=0; i<index; i++) p = p->next; return p->data; } void print_dlist_info(PDLNode head) { printf("The length of the doubly linked list is %d\nData: ", head->data); PDLNode p = head->next; while(p->next){ printf("%d ", p->data); p = p->next; } printf("\n"); } void print_dlist_info_reverse(PDLNode tail) { printf("The length of the doubly linked list is %d\nReverse Data: ", tail->data); PDLNode p = tail->pre; while(p->pre){ printf("%d ", p->data); p = p->pre; } printf("\n"); } int main() { PDLNode head; PDLNode tail; create_list(&head, &tail, 5); print_dlist_info(head); print_dlist_info_reverse(tail); insert(head, 3, 10); print_dlist_info(head); print_dlist_info_reverse(tail); return 0; }
PS: 关于循环链表和静态双向链表我就不一一给出代码,因为他们只是很细微的变化。
相关文章推荐
- Android USB通信学习 USB Host设备通信实际应用
- nginx如何使用预编译变量
- 财务数字转换--大小写转换
- JVM分析工具链(一) - jps和jstack
- 常用的Linux系统调用命令
- 增加在节点上的事件
- 如何制作微信二维码指纹扫描图片
- 我的Ubuntu 环境设置
- 查询ip地址归属地
- 如何通过代理之道UItableView上cell上面的button点击的是哪一行的
- 运行python
- doT js模板入门
- java_easyui体系之DataGrid(4)[转]
- 数据库优化
- 线性顺序表
- mysql利用存储过程批量插入数据
- 操作系统--进程与线程
- 安全站点和非安全站点的 URL 管理
- Ubuntu下挂载Windows/Ubuntu共享目录
- signal signal函数每次设置具体的信号处理函数(非SIG_IGN)只能生效一次,多次调用需要调用时在加类似监听的方法!!! 最好用sigaction