线性链表其他种类(静态,双向,循环)的存储结构和常见操作
一、静态单链表
在不支持动态空间分配的环境中,要使用链表存储数据,那么可采用静态链表的方法:即在一块预分配的存贮空间中,用下标作为指针链来构成链式结构。
//既然是静态链表,那么可以使用一维数组实现存储,java没有指针,那么就用这来使用链表结构
//在不支持动态空间分配的环境中,要使用链式结构技术,可采用静态链表的方法:即在一块预分配的存贮空间中,用下标作为指针。
//存储结构:在数组中增加一个“指针”域,存放下一元素在数组中的下标。且0为代表空指针
//设S为SLinkList型变量,若第i个分量表示链表的第k个结点,则S[i].cur指示第k+1个结点的位置。i=S[i].cur相当于指针后移p=p->next
//头结点数据域空,游标(指示器,指针的意思)是1,代表指向第一个结点,头结点本身下标为0
因为静态分配不会和动态那样,可以手动释放内存,那么如何知道数组里哪些分量没有被使用?
//办法:把所有没有被使用的和被删除的分量用 cur 连接为一个新表叫备用表,插入时,从备用表里取得第一个结点,作为插入结点,删除时把删除的结点链接到备用表
存储结构:
在数组中给每一个元素增加一个“指针”域,(一个数据域,一个“指针”域)存放下一元素在数组中的下标。不改变元素的物理位置!通过增加的指针域的重新链接,改变数组元素的逻辑顺序,且存储空间在运行期间不会动态的改变,也就是是静态链表的实现。
1 // 2 // 静态单链表.h 3 // 单链表的静态存储 4 // 5 // 6 // Copyright (c) 2014年 dashuai. All rights reserved. 7 // 8 9 #ifndef SLIST_H 10 #define SLIST_H 11 #include <stdio.h> 12 #include <stdlib.h> 13 14 #define MAXSIZE 10 15 typedef struct{ 16 int data;//数据域 17 int cur;//指示下一个元素在数据你的下标,相当于指针 18 } Component, SLinkList[MAXSIZE]; 19 20 //初始化静态链(整个数组空间) 21 void initSlist(SLinkList *L); 22 23 // 分配静态链表的结点 24 int mallocSNode(SLinkList *L); 25 26 //释放下表为 n 的结点 27 //其实这里是模拟了动态链表的动态内存分配和释放,库函数 malloc 和 free 28 void freeSNode(SLinkList *L, int n); 29 30 #endif
实现
1 #include "SList.h" 2 3 //初始化静态链(整个数组空间) 4 void initSlist(SLinkList *L) 5 { 6 //创建一个备用链表,存储没有呗使用的结点或者被删除的结点 7 //否则,因为是静态的数组形式,总是插入或者删除,会出现假满假空的现象 8 for (int i = 0; i < MAXSIZE - 1; i++) { 9 L[i]->cur = i + 1; 10 } 11 12 L[MAXSIZE - 1]->cur = 0;//尾结点游标=0,指示是尾结点 13 14 for (int i = 0; i < MAXSIZE; i++) { 15 printf("打印现在的结点data=%d, cur=%d: \n", L[i]->data, L[i]->cur); 16 } 17 } 18 /* 19 因为main 函数里的空闲链表没有初始化,导致内部结构成员有垃圾值 20 */ 21 22 // 分配静态链表的结点,从备用链表里取出 23 int mallocSNode(SLinkList *L) 24 { 25 //指针 i 存储的结点的后继地址 26 int i = L[0]->cur; 27 printf("i = %d\n", i); 28 29 //模拟的动态内存分配过程 30 if (i) { 31 //游标后移一个结点单元,指向当前指向结点的下一个结点 32 // cur 指向下一结点 33 L[0]->cur = L[i]->cur; 34 } 35 36 return i; 37 } 38 39 //释放表 data 为 n 的结点,其实是回收到了备用链表里 40 //其实这里是模拟了动态链表的动态内存分配和释放,库函数 malloc 和 free 41 void freeSNode(SLinkList *L, int n) 42 { 43 //把结点n连接到备用链表上的过程,完全模拟的动态链表,但是其实不是动态的 44 //当前结点 n,指向备用链表头结点的后继 45 L[n]->cur = L[0]->cur; 46 //头结点指向这个当前回收结点 n,以后每次回收,都依次头插 47 L[0]->cur = n; 48 //显然是头插法 49 }
二、循环链表
所谓循环,就是到尾结点,没有空指针,尾结点反而指向了头结点,成环,说白了,就是尼玛头尾张一起了。那么从环中的任意一个结点都能达到表里其他结点,可以循环单恋,也可以多重链起来,俗话说的好,掌握好了单链表的思想和存储,那么一切都是变化罢了,思想没有变。操作大概一样。
差别:
1、循环条件变了,没有空指针,那么循环遍历的终止条件就是看尾指针指向头结点的时候
2、对于循环单链表,又有演化:
如果是头指针表示的循环单链,那么找最后一个元素时间复杂度是 o(n),不过,如果是尾指针表示的,那么找第一个元素是 p->next->next,找最后一个元素是 p 就行了,时间复杂度才是0(1)。
比如:要求合并两个循环单链表A 和 A,该怎么做?
因为是循环的,链表,那么只需要操作两个指针就行,时间复杂度为 o(1)
//此图时间复杂度不是1,应该在这里体现尾指针的方便,需要让两个表的头指针分别变味尾指针,才是操作两个指针(假设是尾指针)
1 //指针 c 指向 A 表的头结点 2 c = a->next; 3 //A 表的尾指针 指向 B 表的首元素,注意不是头结点 4 a->next = b->next->next; 5 //B 表的尾指针指向 c 6 b->next = c; 7 //最后指针合并 8 a = b;
这样是不是非常方便。
三、双链表和循环
单链表的结点,有指示后继的指针域→,找后继结点方便;查找某结点的后继结点的执行时间为O(1)。 没有指示前驱的指针域→,找前驱结点难,从表头出发查找。 即:查找某结点的前驱结点的执行时间为O(n)。这时候双链表应运而生!
双向链表:在单链表的每个结点里再增加一个指向其直接前驱的指针prior ,这样链表中就形成了有两个方向不同的链,故称为双向链表。
存储结构:
1 typedef struct node{ 2 int data; 3 struct node *prior; 4 struct node *next; 5 } node, *doubleLinklist;
双向链表也可以有循环,让头结点的前驱指针指向链表的最后一个结点,让最后一个结点的后继指针指向头结点。 这里需要注意一下空的双向循环链表的表示:
俗话就是自己干自己的情形,说明是空表,只有一个孤单的头结点
双向链表还要一个特点:对称性,比如结点 P,那么存在如下语句
p->prior-next = p;
p->next->prior = p;
双向链表,有些操作 (如:ListLength、GetElem等) ,仅涉及一个方向的指针,算法与线性链表的相同。但插入、删除,则需同时修改两个方向上的指针。这是双向链表的一个难点。
还是用循环双向链表举例:c 99新特性 bool 类型,需要使用#include <stdbool.h>,还有随用虽定义的变量,很爽了,和 c++兼容性越来越强!
1 //初始化双向循环链表 2 //指向指针的指针和返回指针类型,手动分配内存是堆,不是栈,return 栈的内存是错误的,return 堆 么问题! 3 void initDoubleCircleLinklist(doubleLinklist *L) 4 { 5 //l 是头指针 6 //这里标准的写法是这样(林锐语),因为 l 是指针类型,不是不尔类型也不是整型(c 99后来也有了布尔) 7 if (NULL == *L) { 8 9 *L = (doubleLinklist)malloc(sizeof(node));//头指针指向头结点 10 //开始是空, 11 (*L)->next = *L; 12 (*L)->prior = *L; 13 } 14 } 15 16 //求长度 17 int lengthDoubleCircleLinklist(doubleLinklist L) 18 { 19 //默认表已经存在 20 int iNum = 0; 21 doubleLinklist p = L; 22 23 while (p->next != L) { 24 p = p->next; 25 iNum++; 26 } 27 28 return iNum; 29 } 30 31 //判空操作 32 //c99新增bool,为了提高 和c++兼容性 33 bool isEmpty(doubleLinklist L) 34 { 35 return (L->prior == L) && (L->next == L) ? 1 : 0; 36 } 37 38 //找到元素 i 的前驱,并用指针反悔 39 doubleLinklist getIElem(doubleLinklist L, int i) 40 { 41 doubleLinklist p = L; 42 43 for (int j = 1; j < i; j++) { 44 p = p->next; 45 } 46 47 return p; 48 } 49 50 //在循环双链表的第 i 个位置插入一个元素, 51 void insertNode(doubleLinklist L, int i, int nodeElem) 52 { 53 doubleLinklist p = NULL; 54 //先判断合法性 55 if (i > 0 && i <= lengthDoubleCircleLinklist(L) + 1) { 56 //找到 i 的前驱,后继也可以 57 p = getIElem(L, i); 58 //新建结点 59 doubleLinklist s = (doubleLinklist)malloc(sizeof(node)); 60 s->data = nodeElem; 61 //搞定无指针的那一端,不能中途断链,然后再搞另一端 62 s->next = p->next; 63 p->next->prior = s; 64 p->next = s; 65 s->prior = p; 66 } 67 } 68 69 //删除第 i 个元素,并把删除的元素值保存 70 void deleteNode(doubleLinklist L, int i, int *rec) 71 { 72 doubleLinklist p = NULL; 73 if (!(i < 1 || i > lengthDoubleCircleLinklist(L) + 1)) { 74 p = getIElem(L, i); 75 p = p->next; 76 //删除结点 77 *rec = p->data; 78 p->prior->next = p->next; 79 p->next->prior = p->prior; 80 //释放内存,p 指向的内存区域清空,但是 p 没变 81 free(p); 82 //杜绝野指针 83 p = NULL; 84 } 85 else 86 { 87 puts("错误!无法删除"); 88 } 89 } 90 91 //遍历(正) 92 void traverseLinklist(doubleLinklist L) 93 { 94 doubleLinklist p = L->next; 95 96 for (int i = 0; i < lengthDoubleCircleLinklist(L); i++) { 97 printf("%d \t", p->data); 98 p = p->next; 99 } 100 101 putchar('\n'); 102 } 103 104 //遍历(反)一个意思 105 106 107 //销毁 108 void destoryDoubleCircleLinklist(doubleLinklist L) 109 { 110 //一个一个的依次释放,需要两个指示指针 111 doubleLinklist p = NULL; 112 doubleLinklist q = NULL; 113 q = L->next; 114 p = q; 115 //p = q = L; 116 117 while (q != L) { 118 q = q->next; 119 free(p); 120 p = q; 121 } 122 123 free(L); 124 L = NULL; 125 } 126 127 #endif /* defined(____________circualLInked__) */
最重要的就是插入和删除算法!插入和删除两者的操作关键前提步骤就是获得操作对象的前驱的那一步,而表长为 n 的话,那么时间复杂度均为 O(n)。
1 // 2 // main.c 3 // Copyright (c) dashuai. All rights reserved. 4 // 5 #include "circualLInked.h" 6 7 int main(int argc, const char * argv[]) { 8 //建表 9 puts("建循环双向链表"); 10 11 doubleLinklist L; 12 13 initDoubleCircleLinklist(&L); 14 15 //判空 16 puts("循环双向链表初始化之后是空表么?"); 17 if (isEmpty(L)){ 18 puts("表是空的"); 19 } 20 else { 21 puts("表不空"); 22 } 23 24 puts("循环双向链表的表长?"); 25 printf("%d \n", lengthDoubleCircleLinklist(L)); 26 27 puts("插入一些结点"); 28 //最好不要硬编码 29 for (int i = 0; i < 5; i++) { 30 insertNode(L, i + 1, i); 31 //0 1 2 3 4 32 } 33 34 //判空 35 puts("循环双向链表插入之后是空表么?"); 36 if (isEmpty(L)){ 37 puts("表是空的"); 38 } 39 else { 40 puts("表不空"); 41 } 42 43 //头结点不算 44 puts("循环双向链表的表长?"); 45 printf("%d \n", lengthDoubleCircleLinklist(L)); 46 47 //遍历一下看看 48 puts("遍历(正向)循环双向链表"); 49 traverseLinklist(L); 50 51 //删除第二个元素 52 puts("删除第二个结点,1"); 53 int receive = 0; 54 deleteNode(L, 2, &receive); 55 printf("%d \n", receive); 56 57 //遍历一下看看 58 puts("遍历(正向)循环双向链表"); 59 traverseLinklist(L); 60 61 //销毁 62 puts("销毁循环双向链表"); 63 destoryDoubleCircleLinklist(L); 64 65 return 0; 66 }
打印:
建循环双向链表
循环双向链表初始化之后是空表么?
表是空的
循环双向链表的表长?
0
插入一些结点
循环双向链表插入之后是空表么?
表不空
循环双向链表的表长?
5
遍历(正向)循环双向链表
0 1 2 3 4
删除第二个结点,1
1
遍历(正向)循环双向链表
0 2 3 4
销毁循环双向链表
Program ended with exit code: 0
小结:关键字:顺序、链式,静态、动态
1、顺序存储特点:
逻辑顺序与物理顺序一致,本质上是用数组存储线性表的各个元素(即随机存取);存储密度大,存储空间利用率高,可以任意访问任意结点。但是插入删除不方便,需要移动大量元素。是时间换取空间。
2、链式存储特点:
元素之间关系采用元素所在的节点的”指针”信息表示(插、删不需要移动节点)。结点空间可以动态申请和释放,数据元素的逻辑次序靠结点的指针来指示,插入和删除时不需要移动数据元素。链式存储结构的缺点:每个结点的指针域需额外占用存储空间。当数据域所字节不多时,指针域所占存储空间的比重显得很大。链表是非随机存取结构。对任一结点的操作都要从头指针依链查找该结点,这增加了算法的复杂度。不便于在表尾插入元素:需遍历整个表才能找到位置。 链表插入、删除运算的快捷是以空间代价来换取时间。
3、静态存储特点:
在程序运行的过程中不用考虑追加内存的分配问题。
4、动态存储特点:
可动态分配内存,有效利用内存资源,使程序具有可扩展性。
线性表逻辑结构特点:只有一个首结点和尾结点;除首尾结点外其他结点只有一个直接前驱和一个直接后继。线性结构的逻辑关系是一对一(1:1)的。
欢迎关注
dashuai的博客是终身学习践行者,大厂程序员,且专注于工作经验、学习笔记的分享和日常吐槽,包括但不限于互联网行业,附带分享一些PDF电子书,资料,帮忙内推,欢迎拍砖!