双链表、循环链表、静态链表

一、双链表

单链表无法逆向检索,想找到单链表前驱结点会很麻烦,采用可进可退的双链表,存储密度相比单链表低一些。

1.代码定义双链表

typedef struct DNode						//定义双链表结点类型
{
	ElemType data;							//数据域
	struct DNode* prior, * next;			//前驱和后继指针
}DNode,*DLinkList;

双链表结构中包含两个指针域:前驱指针*prior和后继指针*next

2.初始化双链表

bool InitDLinkList(DLinkList& L)
{
	L = (DNode*)malloc(sizeof(DNode));
	if (L == NULL)
		return false;
	L->prior = NULL;						//头结点的prior永远指向NULL
	L->next = NULL;							//头结点之后暂时还没有结点
	return true;
}

初始化双链表需要把头结点的prior和next都指向NULL,如上代码段。

3.双链表判空

判断双链表头结点是否指向NULL即可,如下:

bool Empty(DLinkList L)
{
	if (L->next == NULL)
		return true;
	else
		return false;
}

4.双链表的后插操作

①InsertNextDNode(DNode *p,DNode *s):在p结点之后插入s结点。

②代码实现:

bool InsertNextDNode(DNode* p, DNode* s)
{
	if (p == NULL || s == NULL)				//非法参数
		return false;
	s->next = p->next;
	if (p->next != NULL)						//如果p有后继结点
		p->next->prior = s;
	s->prior = p;
	p->next = s;
	return true;
}

③补充解释:

        (1)先判断p和s的合法性,如果p是NULL,也就意味着不能在空指针后插入;如果s是NULL,也不能说插入一个空的结点;

        (2)把s指向p的后继结点,也就是断连p和p本身的后继结点;

        (3)判断p的后继结点是否为NULL:如果为NULL,即p是表尾结点,那么上一句的执行就是将s的后继指针指向了NULL,此时直接跳过if语句,执行s.prior=p和后续语句,先将s的前驱指针指向p,再将p的后继指针指向s即可;如果p的后继结点不是NULL,即p不是表尾结点,执行if语句,将p的后继结点的前驱指针指向s,再把p和s连上即可。

④双链表的前插以及按位插入等操作都可以用遍历加后插来实现。

5.双链表的遍历

①后向遍历

while (p != NULL)		//双链表的表尾结点后继为空
{
	p = p->next;
}

②前向遍历(没有跳过头结点)

while (p != NULL)		//双链表头结点前驱为空
{
	p = p->prior;
}

③前向遍历(跳过头结点)

while (p->prior != NULL)
{
	p = p->prior;
}

6.双链表的删除

①DeleteNextDNode(DNode *p):删除p结点的后继结点。

②代码实现:

bool DeleteNextDNode(DNode* p)
{
	if (p == NULL)
		return false;
	DNode* q = p->next;						//找到p的后继结点q
	if (q == NULL)
		return false;						//p没有后继结点
	p->next = q->next;
	if (q->next != NULL)					//q结点不是最后一个结点
		q->next->prior = p;
	free(q);								//释放q
	return true;
}

③如果我们想销毁某一个双链表,我们可以依次删除头结点的后继结点,如下:

void DestroyDNodeList(DLinkList& L)
{
	while (L->next != NULL)
		DeleteNextDNode(L);
	free(L);								//释放头结点
	L = NULL;								//头指针指向NULL
}

二、循环链表

1.循环单链表

①普通的单链表最后一个结点指向NULL,循环单链表最后一个结点指回头结点。

typedef struct LNode
{
	int data;
	struct LNode* next;
}LNode,*LinkList;

②初始化一个循环单链表,让头结点的后继指针再指向头结点即可,如下:

bool InitList(LinkList& L)
{
	L = (LNode*)malloc(sizeof(LNode));
	if (L == NULL)
		return false;
	L->next = L;								//头结点next指向头结点
	return true;
}

③循环单链表判空,即判断头结点的后继指针是否指向自身,如下:

bool Empty(LinkList& L)
{
	if (L->next == L)
		return true;
	else
		return false;
}

④判断结点p是否为循环单链表的表尾结点,如果p是表尾结点,那么p的后继指针应该指向头结点,如下:

bool isTail(LinkList L,LNode* p)
{
	if (p->next == L)
		return true;
	else
		return false;
}

⑤相关说明:

        (1)普通单链表从一个结点出发只能找到后续结点,而循环单链表从一个结点出发可以找到其他任何一个结点。

        (2)对于“需要频繁对表头表尾结点进行操作”的情况下,我们可以把循环单链表的头地址指向表尾结点,而不是指向头结点。这样做的好处是,按照常规我们在单链表找头尾的方法,只能从L.next开始依次从头结点扫描到表尾结点,其时间复杂度为O(n);而如果把L指向表尾结点,由于是循环单链表,我们从尾找到头只需要1步即可,此时时间复杂度为O(1),但这时插入或删除操作可能需要修改L的值。

2.循环双链表

①相关概念:

        (1)普通双链表的头结点prior指向NULL,表尾结点的next指向NULL;
        (2)循环双链表的头结点prior指向表尾结点,表尾结点的next指向头结点;
        (3)所有的prior和next指针都形成了一个闭环。

②初始化一个循环双链表;使头结点的前驱和后继指针都指向自身,如下:

bool InitDLinkList(DLinkList& L)
{
	L = (DNode*)malloc(sizeof(DNode));
	if (L == NULL)							//内存不足,分配失败
		return false;
	L->prior = L;							//头结点的prior指向头结点
	L->next = L;							//头结点的next指向头结点
	return true;
}

③循环双链表判空;判断头结点的后继指针是否指向自身,如下:

bool Empty(DLinkList L)
{
	if (L->next == L)
		return true;
	else
		return false;
}

④判断结点p是否为循环双链表的表尾结点;如果p是表尾结点,那么它的后继指针就指向头结点,判断如下:

bool isTail(DLinkList L, DNode* p)
{
	if (p->next == L)
		return true;
	else
		return false;
}

⑤循环双链表的插入;InsertNextDNode(DNode*p,DNode*s):在p结点后插入s结点。

bool InsertNextDNode(DNode* p, DNode* s)
{
	s->next = p->next;
	p->next->prior = s;
	s->prior = p;
	p->next = s;
}

上述代码段和普通双链表的后插操作类似,不同的是这里不用判断p是否为表尾结点,因为循环双链表表尾结点不指向NULL。

⑥循环双链表的删除;DeleteDNode(DNode* q):删除结点q。

bool DeleteDNode(DNode* q)
{
	DNode* q = p->next;
	p->next = q->next;
	q->next->prior = p;
	free(q);
}

通过定义q指向p的后继结点,把p的后继指针指向q的后继结点,改变q后继结点的前驱指针指向p,使pq断连,最后释放p即可。


三、静态链表

1.相关基本概念:

①如上图所示,静态数组分配一整片连续的内存空间,各个结点集中安置;

②每个结点包括一个数据元素(4B)和下一个结点的数组游标(4B)(每个结点共8B),起始地址为addr;

③游标充当指针:在静态数组中,数组下标为0的这个结点充当了“头结点”的角色,头结点的游标指向了下一个结点的数组下标;

④如果某一个结点的游标为-1,即代表这个结点为该静态数组的表尾结点(类似于指向NULL的结点);

⑤指针指向的是某一个具体的地址,而游标指向的是下一个结点的数组下标;

⑥表中某一个结点的存放地址为  addr+8*数组下标。 

2.代码定义静态链表

①第一种定义方式:

#define MaxSize 10				//静态链表的最大长度
struct Node {					//静态链表结构类型的定义
	int data;					//存储数据元素
	int next;					//下一个元素的数组下标
};
void testSLinkList()
{
	struct Node a[MaxSize];		//数组a作为静态链表
}

②第二种定义方式:

#define MaxSize 10
typedef struct {
	int data;
	int next;
}SLinkList[MaxSize];		//等价于 typedef struct Node SLinkList[MaxSize];

③上面的两种定义方式的区别还是在于使用环境不同,实际效果是一样的:
struct Node a[MaxSize];重点强调a是一个Node型数组;SLinkList a;重点强调a是一个静态链表。

3.简述静态链表的操作

①初始化:把a[0]的next设为-1;

②查找:从头结点出发往后挨个遍历结点 O(n);

③插入位序为i的结点:

        (1)找到一个空的结点,存入数据元素(判断空结点时,我们在初始化时可以把这些空闲结点的next设置为某一个特殊值比如-2,那我只需要判断这个结点的游标是不是-2就可以判断这个结点是不是空结点了);

        (2)从头结点出发找到位序为i-1的结点;

        (3)修改新结点的next;

        (4)修改i-1号结点的next;

④删除某个结点:

        (1)从头结点出发找到前驱结点;

        (2)修改前驱结点的游标;

        (3)被删除结点的next设为-2。

4.静态链表优缺点:

①静态链表优点:增删操作不需要大量地移动元素;

②静态链表缺点:不能随机存取,只能从头结点开始依次往后查找,容量固定不可变;

③静态链表使用场景:①不支持指针的低级语言;②数据元素数量固定不变的场景(如操作系统的文件分配表FAT)。

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值