数据结构----单链表

1.顺序表问题与思考

1.1 在正式讲解链表前,我们来看一下顺序表存在的问题,并进行思考。

1.1 问题

(1) 中间/头部的插⼊删除,时间复杂度为O(N).

(2) 增容需要申请新空间,拷⻉数据,释放旧空间,会有不⼩的消耗。
增容⼀般是呈2倍的增⻓,势必会有⼀定的空间浪费。
例如当前容量为100,满了以后增容到200,我们再继续插⼊了5个数据,后⾯没有数据插⼊了,那么就浪费了95个数据空间。

1.2 思考

(1) 如何解决以上问题呢?
(2) 我们可以运用单链表。

2. 单链表

2.1 概念:链表是⼀种物理存储结构上⾮连续、⾮顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
数据结构----单链表_第1张图片
淡季时⻋次的⻋厢会相应减少,旺季时⻋次的⻋厢会额外增加⼏节。只需要将⽕⻋⾥的某节⻋厢去掉/加上,不会影响其他⻋厢,每节⻋厢都是独⽴存在的。
在链表⾥,每节“⻋厢”是什么样的呢?
数据结构----单链表_第2张图片
2.2 结点:与顺序表不同的是,链表⾥的每节"⻋厢"都是独⽴申请下来的空间,我们称之为结点。
结点的组成主要有两个部分:
(1) 当前结点要保存的数据.
(2) 保存下⼀个结点的地址(指针变量).

2.3 图中指针变量plist保存的是第⼀个结点的地址,我们称plist此时“指向” 第⼀个结点,如果我们希望plist“指向” 第⼆个结点时,只需要修改plist保存的内容为0x0012FFA0。

2.4 链表中每个结点都是独⽴申请的(即需要插⼊数据时才去申请⼀块结点的空间),我们需要通过指针变量来保存下⼀个结点位置才能从当前结点找到下⼀个结点。

3. 链表的性质

1. 链式机构在逻辑上是连续的,在物理结构上不⼀定连续。
2. 结点⼀般是从堆上申请的。
3. 从堆上申请来的空间,是按照⼀定策略分配出来的,每次申请的空间可能连续,可能不连续。

结合前⾯学到的结构体知识,我们可以给出每个结点对应的结构体代码:假设当前保存的结点为整型:

struct SListNode
{
   int data;       //结点数据
  struct SListNode* next; //指针变量⽤保存下⼀个结点的地址
};

1. 当我们想要保存⼀个整型数据时,实际是向操作系统申请了⼀块内存,这个内存不仅要保存整型数据,也需要保存下⼀个结点的地址(当下⼀个结点为空时保存的地址为空)

2. 当我们想要从第⼀个结点⾛到最后⼀个结点时,只需要在当前结点拿上下⼀个结点的地址就可以了

4. 链表的打印

给定的链表结构中,如何实现结点从头到尾的打印?
数据结构----单链表_第3张图片
思考:当我们想保存的数据类型为字符型、浮点型或者其他⾃定义的类型时,该如何修改?

5. 链表的实现

在实现链表时,我们也需要创建三个文件分别为 SList.h,SList.c,test.c三个文件。

注:这里的命名依然是根据英文单词命名。 Single:单个的,单一的。List:列表。
SList 即代表单链表的意思。

SList.h 用于定义单链表结构,声明和提供函数。
它像书的目录一样,一眼就知道具体有哪些需要实现的操作,要完成的内容。

SList.c 用于实现头文件中所声明的函数。

test.c 用于测试 SList.c 中所实现的函数。
因为不知道所写的 SList.c 中的具体实现操作是否正确,所以我们需测试,修改错误代码。
这里的文件创建与实现顺序表时的文件创建是一模一样的。
SList.h

#pragma once//防止头文件多次包含
#include
#include//动态申请所需头文件
#include//断言头文件

//定义链表(结点)的结构
思考:当我们想保存的数据类型为字符型、浮点型或者其他⾃定义的类型时,该如何修改?
typedef int SLTDataType;//给int重命名,这里与顺序表一样,这也完成了思考时的疑问,我们只需把int修改成我们想保存的数据类型即可,此时链表就可正常使用。
typedef struct SListNode {
 SLTDataType data;
 struct SListNode* next;
}SLTNode;//给结构体重命名,也与顺序表写法类似

void SLTPrint(SLTNode* phead);//打印链表

//插入
void SLTPushBack(SLTNode** pphead, SLTDataType x);//尾插
void SLTPushFront(SLTNode** pphead, SLTDataType x);//头插

//删除
void SLTPopBack(SLTNode** pphead);//尾删
void SLTPopFront(SLTNode** pphead);//头删

//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x);//查找值为x的数据

//在指定位置之前插⼊数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x);
//在指定位置之后插⼊数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x);

//删除pos结点
void SLTErase(SLTNode * *pphead, SLTNode * pos);
//删除pos之后的结点
void SLTEraseAfter(SLTNode * pos);
//销毁链表
void SListDestroy(SLTNode * *pphead);

SList.c

#include"SList.h"//包含单链表的头文件

void SLTPrint(SLTNode* phead)//打印链表
{
  SLTNode* pcur = phead;//定义新的变量遍历链表,如果用phead遍历的话,phead会走到最后,
  找不到链表的第一个节点,虽说打印链表并不需要找链表的第一个节点,
  但我们不代表后续我们不需要找链表的第一个节点,所以我们为了保持良好习惯,定义一个新变量。
  while (pcur)//遍历链表
  {
  	printf("%d->", pcur->data);//打印
  	pcur = pcur->next;//pcur走到链表的下一个节点
  }
  printf("NULL\n");遍历结束后我们最后一个节点的next保存的是NULL,所以我们打印一下。
}

//申请新节点
SLTNode* SLTBuyNode(SLTDataType x)
{
  SLTNode* node = (SLTNode*)malloc(sizeof(SLTNode));//动态申请新节点
  if (node == NULL)//判断动态申请是否申请失败
  {
  	perror("malloc fail!");//打印错误信息
  	exit(1);//退出程序
  }
  node->data = x;//申请成功,把x赋值给节点要保存的数据
  node->next = NULL;//申请新节点的next中没有下一个结点的地址,我们设置为空。
  return node;//返回新节点
}

void SLTPushBack(SLTNode** pphead, SLTDataType x)//尾插
{
  assert(pphead);//断言,防止对空指针解引用。
  //pphead --> &plist//传的实参为plist的地址,也就是一级指针变量的地址,需用二级指针接收, 下面的test.c方法中会进行传参                   
  // *pphead --> plist
  SLTNode* newnode = SLTBuyNode(x);//申请新节点
  if (*pphead == NULL)//判断链表是否为空
  {
  	*pphead = newnode;//如果链表初始就为空,那么新申请的节点就是我们链表的第一个节点,也是我们尾插的数据。
  	                    
  }
  else
  {
  	//如果链表不为空,找尾结点
  	SLTNode* pcur = *pphead;
  	while (pcur->next)//尾节点的next指针保存的是NULL
  	{
  		pcur = pcur->next;
  	}
  	pcur->next = newnode;把我们新申请的节点尾插到链表当前的尾节点
  }
}
void SLTPushFront(SLTNode** pphead, SLTDataType x)//头插
{
  assert(pphead);//断言,防止对空指针解引用。
  SLTNode* newnode = SLTBuyNode(x);//申请新节点
  newnode->next = *pphead;//这里不需要判断链表是否为空,如果链表为空我们就把NULL赋值给新节点的next指针,此时我们的新结点就是我们链表的第一个节点,也是我们头插的数据,链表不为空就把当前链表的第一个结点的地址赋值给新节点的next指针。
  *pphead = newnode;//最后让指向链表第一个节点的指针,指向我们的新结点,保存新节点的地址,此时就完成了头插。
}
void SLTPopBack(SLTNode** pphead)//尾删
{
  //链表为空:不可以删除
  assert(pphead && *pphead);
  //处理只有一个结点的情况:要删除的就是头结点	
  if ((*pphead)->next == NULL)
  {
  	free(*pphead);//释放*pphead所指向的节点空间,此时*pphead保存的是无效的地址,因为动态申请的空间已经还给操作系统了,*pphead变成了野指针
  	*pphead = NULL;//防止后续对野指针进行解引用。
  }
  else
   {
  	//找 prev ptail
  	SLTNode* ptail = *pphead;
  	SLTNode* prev = NULL;
  	while (ptail->next)//找尾节点
  	{
  		prev = ptail;//保存尾节点的前一个结点的地址
  		ptail = ptail->next;//继续向后遍历
  	}
  	prev->next = NULL;//删除尾节点
  	free(ptail);//释放空间,此时ptail保存的是无效的地址,因为动态申请的空间已经还给操作系统了,ptail变成了野指针。
  	ptail = NULL;//防止后续可能对野指针进行解引用
   }
}
void SLTPopFront(SLTNode** pphead)//头删指向
{
  assert(pphead && *pphead);//链表为空:不可以删除
  SLTNode* next = (*pphead)->next;//保存链表第一个节点的下一个结点的地址
  free(*pphead);//这个与上述一样,释放空间
  *pphead = next;//让指向链表第一个节点的指针,指向链表的下一个结点。
}
//查找
SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
  assert(phead);断言,防止对空指针解引用。
  SLTNode* pcur = phead;
  while (pcur)
  {
  	if (pcur->data == x)//找到了
  	{
  		return pcur;//返回该节点
  	}
  	pcur = pcur->next;
  }
  //没有找到
  return NULL;
}
//在指定位置之前插⼊数据
void SLTInsert(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
  assert(pphead);
  assert(pos);//插入的位置必须有效,不可为空
  
  if (pos == *pphead)//pos的位置正好是当前链表第一个节点的位置
  {
  	SLTPushFront(pphead, x);//头插就是在指定位置之前插⼊数据
  }
  else
  {
  	SLTNode* newnode = SLTBuyNode(x);//申请新节点
  	//找prev :pos的前一个结点
  	SLTNode* prev = *pphead;
  	while (prev->next != pos)
  	{
  		prev = prev->next;
  	}
  	newnode->next = pos;//完成/在指定位置之前插⼊数据
  	prev->next = newnode;//让pos的前一个结点的next指针指向新节点
  }
}
//在指定位置之后插⼊数据
void SLTInsertAfter(SLTNode* pos, SLTDataType x)
{
  assert(pos);//插入的位置必须有效,不可为空
  SLTNode* newnode = SLTBuyNode(x);
  newnode->next = pos->next;//新节点的next指针指向pos的下一个节点
  pos->next = newnode;//pos的next指针指向新节点
}
//删除pos结点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
  assert(pphead && *pphead);
  assert(pos);
  if (pos == *pphead)//pos的位置正好是当前链表的第一个节点的位置
  {
  	SLTPopFront(pphead);//此时头删就好
  }
  else
  {
  	SLTNode* prev = *pphead;
  	while (prev->next != pos)//找到pos位置的前一个节点
  	{
  		prev = prev->next;
  	}
  	prev->next = pos->next;//pos位置的前一个节点的指针指向pos的位置的下一个节点,此时相当于删除了pos
  	free(pos);
  	pos = NULL;
  }
}
//删除pos之后的结点
void SLTEraseAfter(SLTNode* pos)
{
  assert(pos && pos->next);//pos的位置不为空,并且pos的下一个位置也不能为空
  SLTNode* del = pos->next;//保存pos之后节点
  pos->next = pos->next->next;//pos位置节点的next指针指向pos的位置的下一个节点的下一个节点,此时相当于删除了pos之后的节点
  free(del);
  del = NULL;
}
//销毁链表
void SListDestroy(SLTNode** pphead)
{
  assert(pphead && *pphead);//链表不为空
  SLTNode* pcur = *pphead;
  while (pcur)
  {
  	SLTNode* next = pcur->next;//保存链表第一个节点的下一个节点的地址
  	free(pcur);//释放pcur所指向的空间
  	pcur = next;//让pcur走到一开始next保存好的下一个节点的位置,然后继续遍历销毁,循环重复此过程,直到跳出循环。
  	那为什么这么做,而不像之前那样继续遍历呢?
  	因为此时pcur所指向的空间已经被释放掉了,pcur为野指针了,不能继续访问该节点的next指针并向后遍历。
  }
  *pphead = NULL;//*pphead所指向的节点空间被销毁,变成了野指针,防止后续对野指针进行解引用。
  }

test.c

#include"SList.h"
void SListTest01()
{
  SLTNode* plist = NULL;
  SLTPushBack(&plist, 1);//传一级指针变量的地址,因为尾插会改变原链表,所以需要传地址
  SLTPushBack(&plist, 2);
  SLTPushBack(&plist, 3);
  SLTPushBack(&plist, 4);
  SLTPrint(plist);//1->2->3->4->NULL
  SLTPopBack(&plist);
  SLTPrint(plist);//1->2->3->NULL
  SLTPopFront(&plist);
  SLTPrint(plist);//2->3->NULL
  SLTNode* find = SLTFind(plist, 3);//查找值为4的值
  if (find == NULL)
  {
   printf("未找到!\n");
  }
  else
  {
  	printf("找到了!\n");
  }
  SLTInsert(&plist, find, 11);//2->11->3->NULL
  SLTInsertAfter(find, 10);//2->11->3->10->NULL
  
  //SLTErase(&plist, find);// 2->11->10->NULL
  SLTEraseAfter(find);//2->11->3->NULL
  SListDestroy(&plist);//销毁链表
}

你可能感兴趣的:(数据结构)