开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第5天,点击查看活动详情
2.1.4 链式存储及查找
线性表的链式存储实现
-
不要求逻辑上相邻的两个元素物理上也相邻;通过"链"建立起数据元素之间的逻辑关系
- 插入、删除不需要移动数据元素,只需要修改"链"
typedef struct LNode *List;
struct LNode{
ElementType Data;
List Next;
};
struct Lnode L;
List PtrL;
主要操作的实现
-
求表长
int Length(List PtrL)//链表的头指针,并且是单向链表 { List p = PtrL;//p指向表的第一个结点 int j = 0; while(p){ p = p->Next;//这步操作相当于让指针往后挪一位 j++;//当前p指向的是第j个结点 } return j; } //时间性能为O(n) -
查找
//(1)按序号查找:FindKth;采用类似链表的遍历方法 List FindKth(int K,List PtrL) { List p = PtrL;//首先把p这个临时变量设置为链表的表头 int i = 1; while(p != NULL && i < K){ p = p->Next;//让指针往后挪一位 i++; } if( i == K ) return p;//找到第K个,返回指针 else return NULL;//否则返回空 } //(2)按值查找:Find List Find(ElementType X,List PtrL) { List p = PtrL; while(p != NULL && p -> Data != X) p = p->Next; return p; } //返回的是两种结果,不是p就是NULL,调用Find函数,发现它返回值等于NULL,就说明没找着2.1.5链式存储的插入和删除
-
插入(在第i-1(1 <= i <= n+1)个结点后插入一个值为X的新结点)
//(1)先构造一个新结点,用s指向; 这个时候可以用malloc这个函数来申请一块空间 //(2)再找到链表的第i-1个结点,用p指向; //(3)然后修改指针,插入结点(p之后插入新结点是s) //下图中的操作步骤:让s指向下一个结点,p的Next附给s的Next //如果修改指针的两个步骤交换了一下哎,会发生什么?(语句执行顺序为:(1) p->Next=s; (2) s->Next=p->Next;) //答案:s->Next指向s,从而不能正确完成插入//malloc复习区域 #include<malloc.h>或者#include<alloc.h>//两者的内容是完全一样的 如果分配成功:则返回指向被分配内存空间的指针 不然返回指针NULL 同时,当内存不再使用的时候,应使用free()函数将内存块释放掉。 关于:void*,表示未确定类型的指针,c,c++规定void*可以强转为任何其他类型的指针,关于void还有一种说法就是其他任何类型都可以直接赋值给它,无需进行强转,但是反过来不可以 malloc: malloc分配的内存大小至少为参数所指定的字节数 malloc的返回值是一个指针,指向一段可用内存的起始位置,指向一段可用内存的起始地址,多次调用malloc所分配的地址不能有重叠部分,除非某次malloc所分配的地址被释放掉malloc应该尽快完成内存分配并返回(不能使用NP-hard的内存分配算法)实现malloc时应同时实现内存大小调整和内存释放函数(realloc和free) malloc和free是配对的,如果申请后不释放就是内存泄露,如果无故释放那就是什么也没做,释放只能释放一次,如果一块空间释放两次或者两次以上会出现错误(但是释放空指针例外,释放空指针也等于什么也没做,所以释放多少次都是可以的。) 2、malloc和new new返回指定类型的指针,并且可以自动计算所需要的大小。 int *p; p = new int;//返回类型为int* ,分配的大小是sizeof(int) p = new int[100];//返回类型是int*类型,分配的大小为sizeof(int)*100 而malloc需要我们自己计算字节数,并且返回的时候要强转成指定类型的指针。 int *p; p = (int *)malloc(sizeof(int)); 1)malloc的返回是void*,如果我们写成了:p=malloc(sizeof(int));间接的说明了(将void转化给了int*,这不合理) (2)malloc的实参是sizeof(int),用于指明一个整型数据需要的大小,如果我们写成p=(int*)malloc(1),那么可以看出:只是申请了一个一个字节大小的空间。 (3)malloc只管分配内存,并不能对其进行初始化,所以得到的一片新内存中,其值将是随机的。一般意义上:我们习惯性的将其初始化为NULL,当然也可以使用memset函数。 简单的说: malloc函数其实就是在内存中找一片指定大小的空间,然后将这个空间的首地址给一个指针变量,这里的指针变量可以是一个单独的指针,也可以是一个数组的首地址,这要看malloc函数中参数size的具体内容。我们这里malloc分配的内存空间在逻辑上是连续的,而在物理上可以不连续。我们作为程序员,关注的是逻辑上的连续,其他的操作系统会帮着我们处理。 下面就来看看malloc具体是怎么实现的。 首先要了解操作系统相关的知识: 虚拟内存地址和物理内存地址 为了简单,现代操作系统在处理物理内存地址时,普遍采用虚拟内存地址技术。即在汇编程序层面,当涉及内存地址时,都是使用的虚拟内存地址。采用这种技术时,每个进程仿佛自己独享一片2N字节的内存,其中N是机器位数。例如在64位CPU和64位操作系统下每个进程的虚拟地址空间为264Byte。 这种虚拟地址空间的作用主要是简化程序的编写及方便操作系统对进程间内存的隔离管理,真实中的进程不太可能如此大的空间,实际能用到的空间大小取决于物理内存的大小。 由于在机器语言层面都是采用虚拟地址,当实际的机器码程序涉及到内存操作时,需要根据当前进程运行的实际上下文将虚拟地址转化为物理内存地址,才能实现对内存数据的操作。这个转换一般由一个叫MMU的硬件完成。插入实现操作
List Insert(ElementType X,int i,List PtrL) { List p,s; if(i == 1){//新结点插入在表头 s = (List)malloc(sizeof(struct LNode));//申请、填装结点 s -> Data = X; s -> Next = PtrL; return s;//返回新表头指针 } p = FindKth(i-1.PtrL);//查找第i-1个结点 if( p == NULL){//第i-1个不存在,不能插入 printf("参数i错"); return NULL; }else{ s = (List)malloc(sizeof(struct LNode));//申请、填装结点 s -> Data = X; s -> Next = p -> Next;//新结点插入在第i-1个结点的后面 p -> Next = s; return PtrL; //这种情况下链表的头指针是不会变的 } } //平均查找次数是n/2删除(删除链表的第i(1 <= i <= n)个位置上的结点)
//(1)先找到链表的第i-1个结点,用p指向; //(2)再用指针s指向要被删除的结点(p的下一个结点); //(3)然后修改指针,删除s所指结点; //(4)删除的结点(s)的空间要记得free释放掉(重要),这样内存空间才不会泄漏 List Delete(int i,List PtrL) { List p,s; if( i == 1 ){//若要删除的是表的第一个结点 s = PtrL;//s指向第一个结点 if(PtrL != NULL) PtrL = PtrL -> Next; else return NULL; free(s);//释放掉被删除结点 return PtrL; } p = FindKth(i-1,PtrL); //查找第i-1个结点,就是要删除结点的前一个结点在哪里 if(p == NULL){ printf("第%d个结点不存在",i-1); return NULL; }else if(p -> Next == NULL){ printf("第%d个结点不存在",i); return NULL; }else{ s = p -> Next;//s指向第i个结点 p -> Next = s -> Next;//从链表中删除 free(s);//释放被删除结点 return PtrL; } } //平均时间复杂度也是n/2
2.1.6 广义表与多重链表
原本a,b,c所在的位置变成了指针,指向另一个一元多项式。这种就是广义表
广义表(Generalized List)
-
广义表是线性表的推广
-
对于线性表而言,n个元素都是基本的单元素;
-
广义表中,这些元素不仅可以是单元素也可以是另一个广义表
-
广义表可能会碰到的问题:一个域有可能不能分解的单元,有可能是一个指针(C语言的解决方法是使用union(联合))
-
union(联合):可以把不同类型的数据组合在一起,可以把这个空间理解成某种类型,也可以理解为另外一种类型
-
区分类型的方法:再弄个标记
-
typedef struct GNode *GList; struct GNode{ int Tag;//标志域:0表示结点时单元素,1表示结点是广义表 这个Tag就是标志 union{//子表指针域Sublist与单元素数据域Data复用,即共同存储空间 ElementType Data; GList SubList; }URegion; GList Next;//指向后续结点 };
多重链表
多重链表:链表中的节点可能同时隶属于多个链
- 多重链表中结点的指针域会有多个,如前面例子包含了Next和SubList两个指针域;
- 但包含两个指针域的链表并不一定是多重链表,比如在双向链表不是多重链表。
多重链表有广泛的用途:基本上如树,图这样相对复杂的数据结构都可以采用多重链表的方式实现存储
多重链表指的是它里面的这个链表的结点可能同时隶属于多个链表(意思就是表中的指针会有多个)
稀疏矩阵:矩阵中的0很多,会造成空间浪费
二维数组可以用来表示选课的一种记录
上图中就是用多重链表来表示稀疏矩阵的一种方法
上图中的行与列相互穿插在一起形成十字链表
Head是作为行这个链表的头结点,也作为列这个链表的头结点
Tem:代表稀疏矩阵里面的非零的项
上图:4代表这个稀疏矩阵共有4行,总共有5列,非零项个数总共有7项
通过上图那个指针就可以找到所有列的头节点
在矩阵的多重链表表示中,第i行的head和第i列的head实际上是同一个结点(正确)
-
用一个标识域Tag来区分头结点和非0元素结点
-
头节点的标识值为"Head",矩阵非0元素结点的标识值为"Term"
经过union的串联在一起,他们共性都是有两个指针:一个Down,一个Right。他们不一样的地方在中间部分。所有我们可以把他们union在一起,形成(a)这个结构
以上就是稀疏矩阵用十字链表解决的一种基本思路