数据结构——链表 (单向不带头不循环链表 与 双向带头循环链表)

目录

一、类型

1、单链表、双向链表

2、不带头单链表、带头链表

3、单链表、循环链表

二、单向+ 不带头+ 不循环链表

链表接头

函数定义:

1、打印链表

2、新节点的创建

3、尾插节点

     3.1、非空情况

     3.2、空链表情况

4、头插节点

5、尾删(三种情况)

5.1、没有节点

5.2、一个节点

5.3、多个节点

6、头删

6.1、多个节点

6.2、单个节点

7、查找节点

7、插入pos节点的后方

8、插入pos的前方

8.1、pos是链表第一个节点

8.2、pos是链表中间节点

9、删除pos节点后的节点

三、双向+ 带头+ 循环链表

链表接头

函数定义:

1.打印单链表

2、新节点的创建

2.1、新节点的初始化

3、尾插节点

     3.1、非空链表情况

4、头插节点

5、尾删

5.1、没有节点

5.2、一个节点、多个节点

6、头删

6.1、多个节点、单个节点

​编辑

7、查找节点

7、插入pos节点

8、删除pos节点后的节点

9、检查链表(空返回1,非空返回0)

10、计算链表有效值个数

11、清空链表

12、双向链表总结

顺序表和链表的优缺点对比


一、类型

1、单链表、双向链表

数据结构——链表 (单向不带头不循环链表 与 双向带头循环链表)_第1张图片

2、不带头单链表、带头链表

数据结构——链表 (单向不带头不循环链表 与 双向带头循环链表)_第2张图片

3、单链表、循环链表

数据结构——链表 (单向不带头不循环链表 与 双向带头循环链表)_第3张图片

  • 可以结合共成八种

二、单向+ 不带头+ 不循环链表

链表的定义结构

typedef int SLTDataType;//方便后期修改数据类型
typedef struct SListNode
{
	SLTDataType data;      //数据域
	struct SListNode* next;//指针域
}SLTNode;

  • 链表接头

接头函数声明

//单项+ 不带头+ 不循环
void SListPrint(SLTNode* plist);

void SListPushBack(SLTNode** pplist, SLTDataType x);//尾插
void SListPushFront(SLTNode** plist, SLTDataType x);//头插

void SListPopBack(SLTNode** pplist);//尾删
void SListPopFront(SLTNode** pplist);//头删

SLTNode* SListFind(SLTNode* plist, SLTDataType x);//搜索

void SListInsertAfter(SLTNode* pos, SLTDataType x);//任意插入
void SListInsertBefore(SLTNode** pplist, SLTNode* pos, SLTDataType x);
//任意插入pos位置前

void SListEraseAfter(SLTNode* pos);//删除pos后节点
//void SListEraseCur(SLTNode* pos);//删除pos

函数定义:

 1.打印链表

void SListPrint(SLTNode* plist)
{
       SLTNode* cur = plist;
       while (cur != NULL)
       {
              printf("%d ", cur->data);
              cur = cur->next;
       }
       printf("\n");
}

2、新节点的创建

//将所需数据放入节点里,return 创建的地址,需要一个指针接收
SLTNode* BuySLTNode(SLTDataType x)
{
       SLTNode* node = (SLTNode*)malloc(sizeof(SLTNode));
       node->data = x;
       node->next = NULL;
       return node;
}

3、尾插节点

     3.1、非空情况

数据结构——链表 (单向不带头不循环链表 与 双向带头循环链表)_第4张图片

     3.2、空链表情况

数据结构——链表 (单向不带头不循环链表 与 双向带头循环链表)_第5张图片

void SListPushBack(SLTNode** pplist, SLTDataType x)//尾插
{
       //创建节点存储
       SLTNode* newnode = BuySLTNode(x);
       //1、空的情况
       //2、非空情况
       if (*pplist == NULL)
       {
              *pplist = newnode;//修改的是地址,且函数无return,所以用二级指针
       }
       else
       {
              //找尾
              SLTNode* tail = *pplist;
              while (tail->next != NULL)
              {
                      tail = tail->next;
              }
              tail->next = newnode;//用节点尾巴去接上新节点
       }
}

4、头插节点

数据结构——链表 (单向不带头不循环链表 与 双向带头循环链表)_第6张图片

void SListPushFront(SLTNode** pplist, SLTDataType x)//头插
{
       SLTNode* newnode = BuySLTNode(x);
       newnode->next = *pplist; //先将new的next指向pplist原本指向的节点
       *pplist = newnode;       //再将new节点的地址设置为pplist
}

5、尾删(三种情况)

5.1、没有节点

    -直接return,或者断言

5.2、一个节点

数据结构——链表 (单向不带头不循环链表 与 双向带头循环链表)_第7张图片

将其free再置空

5.3、多个节点

数据结构——链表 (单向不带头不循环链表 与 双向带头循环链表)_第8张图片

void SListPopBack(SLTNode** pplist)
{
	//1、没有节点
	//2、一个节点
	//3、多个节点
	if (*pplist == NULL)
	{
		return;
	}
	else if((*pplist)->next == NULL)
	{
		free(*pplist);
		*pplist = NULL;
	}
	else
	{
		SLTNode* prev = NULL;//前驱指针
		SLTNode* tail = *pplist;
		while (tail->next != NULL)
		{
			prev = tail;       //前驱后移
			tail = tail->next; //tail也后移
		}
		free(tail);//将tail置空,此时plist的最后一个节点也被置空
		tail = NULL;//防止野指针

		prev->next = NULL;//将尾指针置空
	}
	
}

6、头删

6.1、多个节点

数据结构——链表 (单向不带头不循环链表 与 双向带头循环链表)_第9张图片

6.2、单个节点

数据结构——链表 (单向不带头不循环链表 与 双向带头循环链表)_第10张图片

void SListPopFront(SLTNode** pplist)
{
	if (*pplist == NULL)
	{
		return;
	}
	else
	{
		SLTNode* next = (*pplist)->next;//制定一个next存放首节点后的节点
		free(*pplist);

		*pplist = next;
	}
}

7、查找节点

//查找,返回节点x的指针
SLTNode* SListFind(SLTNode* plist, SLTDataType x)
{
	SLTNode* cur = plist;
	while (cur)
	{
		if (cur->data == x)
		{
			return cur;
		}
		cur = cur->next;
	}

	return NULL;
}

7、插入pos节点的后方

数据结构——链表 (单向不带头不循环链表 与 双向带头循环链表)_第11张图片

void SListInsertAfter(SLTNode* pos, SLTDataType x)//任意插入
{
	assert(pos);

	SLTNode* newnode = BuySLTNode(x);
	newnode->next = pos->next;//先将新封装的节点连接到d3
	pos->next = newnode;//再讲d2的next连接到新节点
}

8、插入pos的前方

8.1、pos是链表第一个节点

数据结构——链表 (单向不带头不循环链表 与 双向带头循环链表)_第12张图片

8.2、pos是链表中间节点

数据结构——链表 (单向不带头不循环链表 与 双向带头循环链表)_第13张图片

//任意插入pos位置前
void SListInsertBefore(SLTNode** pplist, SLTNode* pos, SLTDataType x)
{
	//只要涉及到改变第一个节点,也就是要修改传入的指针变量
	//那么就要用二级指针
	assert(pos);

	SLTNode* newnode = BuySLTNode(x);

	if (pos == *pplist)//就是头插
	{
		newnode->next = pos;
		*pplist = newnode;//将新节点指定为第一个节点,用二级指针传值
	}
	else
	{
		SLTNode* prev = NULL;
		SLTNode* cur = *pplist;
		while (cur != pos)
		{
			prev = cur;
			cur = cur->next;
		}
		prev->next = newnode;
		newnode->next = pos;
	}
}

思考:

在一个无头单链表的某一节点的前面插入一个值x,如何插入?(不告诉头指针)

  • 后插入一个值为x的节点,然后和前面的节点值data交换

数据结构——链表 (单向不带头不循环链表 与 双向带头循环链表)_第14张图片

9、删除pos节点后的节点

对于两种情况,一种解决方案即可

数据结构——链表 (单向不带头不循环链表 与 双向带头循环链表)_第15张图片

void SListEraseAfter(SLTNode* pos)//删除节点
{
	assert(pos);
	if (pos->next == NULL)
	{
		return;
	}
	else
	{
		SLTNode* next = pos->next;//用next存放d3
		pos->next = next->next;//再将pos连接到d3的后一节点
		free(next);//释放d3
	}
}

10、删除当前pos

思路:

数据结构——链表 (单向不带头不循环链表 与 双向带头循环链表)_第16张图片


三、双向+ 带头+ 循环链表

链表的定义结构

typedef int LTDataType;
typedef struct ListNode
{
	struct ListNode* next;
	struct ListNode* prev;//前驱指针
	LTDataType data;
}ListNode;

  • 链表接头

接头函数声明

ListNode* BuyListNode(LTDataType x);//创建节点

ListNode* ListInit();//初始化节点
void ListPrint(ListNode* phead);//打印链表

void ListPushBack(ListNode* phead, LTDataType x);//尾插
void ListPushFront(ListNode* phead, LTDataType x);
void ListPopBack(ListNode* phead);
void ListPopFront(ListNode* phead);

ListNode* ListFind(ListNode* phead, LTDataType x);
void ListInsert(ListNode* pos, LTDataType x);
void ListErase(ListNode* pos);

//空返回1,非空返回0
int ListEmpty(ListNode* phead);
int ListSize(ListNode* phead);
void ListDestory(ListNode* phead);

函数定义:

1.打印单链表

void ListPrint(ListNode* phead)
{
	ListNode* cur = phead->next;
	while (cur != phead)
	{
		printf("%d ", cur->data);
		cur = cur->next;
	}
	printf("\n");
}

2、新节点的创建

//将所需数据放入节点里,return 创建的地址,需要一个指针接收
ListNode* BuyListNode(LTDataType x)
{
	ListNode* node = (ListNode*)malloc(sizeof(ListNode));
	node->next = NULL;
	node->prev = NULL;
	node->data = x;
	return node;
}

2.1、新节点的初始化

  • 双向带头循环链表在初始化只有一个头节点时,要见头尾指向自己
ListNode* ListInit()
{
	ListNode* phead = BuyListNode(0);
	phead->next = phead;//让头指向指向自己
	phead->prev = phead;//让尾指向指向自己

	return phead;
}

3、尾插节点

     3.1、非空链表情况

在只有一个头节点或者多个节点都可以一次实现

数据结构——链表 (单向不带头不循环链表 与 双向带头循环链表)_第17张图片

void ListPushBack(ListNode* phead, LTDataType x)
{
	assert(phead);
	//初链表只有一个节点或者多节点都可以实现

	ListNode* tail = phead->prev;//因为循环,所以头节点的前一个是为节点,用tail记录
	ListNode* newnode = BuyListNode(x);

	tail->next = newnode;//将原尾节点tail连接新节点
	newnode->prev = tail;//新节点的前驱节点设置为tail
	newnode->next = phead;//新节点的下一节点设置为头节点
	phead->prev = newnode;//头节点的前驱指针指向新节点

	//此方法也是尾插
	/*ListInsert(phead, x);*/ 

}

4、头插节点

数据结构——链表 (单向不带头不循环链表 与 双向带头循环链表)_第18张图片

void ListPushFront(ListNode* phead, LTDataType x)
{
	assert(phead);
	//头插注意要先将newnode先连接第一有数据节点(不是头指针)
	//或者设置一个新的指针(frist)先存放第一有数据节点
	ListNode* first = phead->next;
	ListNode* newnode = BuyListNode(x);

	//开始连接三个指针
	phead->next = newnode;
	newnode->prev = phead;
	newnode->next = first;
	first->prev = newnode;

	//此方法也是头插
	/*ListInsert(phead->next, x);*/
}

5、尾删

5.1、没有节点

    删除要注意断言只有头指针的情况

5.2、一个节点、多个节点

两种情况一种解决方案

数据结构——链表 (单向不带头不循环链表 与 双向带头循环链表)_第19张图片

void ListPopBack(ListNode* phead)
{
	//不用遍历即可找到尾项
	assert(phead);
	assert(phead->next != phead);//如果链表只有头节点在此情况也是错误链表

	ListNode* tail = phead->prev;//设置指针指向原尾项
	ListNode* tailPrev = tail->prev;//设置指针指向倒数第二项
	free(tail);//释放最后一项

	tailPrev->next = phead;//倒数第二项晋升为最后一项,用其连接头节点
	phead->prev = tailPrev;//将头节点的前驱指针指向新尾节点

	//此方法也是尾插
	/*ListErase(phead->prev);*/
}

6、头删

6.1、多个节点、单个节点

两种情况一种解决方案

数据结构——链表 (单向不带头不循环链表 与 双向带头循环链表)_第20张图片

void ListPopFront(ListNode* phead)
{
	assert(phead);
	assert(phead->next != phead);//如果链表只有头节点也是错误链表

	ListNode* frist = phead->next;
	ListNode* fristNext = frist->next;
	free(frist);

	fristNext->prev = phead;
	phead->next = fristNext;

	//此方法也是头插
	/*ListErase(phead->next);*/

}

7、查找节点

ListNode* ListFind(ListNode* phead, LTDataType x)
{
	assert(phead);
	ListNode* cur = phead->next;
	while (cur != phead)
	{
		if (cur->data == x)
			return cur;

		cur = cur->next;
	}
	return NULL;//没找到返回空
}

7、插入pos节点

  • 插入前(对于双向链表在pos前后插入节点都很简单)
  • 就算pos是头节点 或者 是最后一个节点 都很容易实现
  • 不过pos是头节点用此方法相当于 尾插
  • pos是phead->next则相当于 头插

数据结构——链表 (单向不带头不循环链表 与 双向带头循环链表)_第21张图片

//插入前(对于双向链表在pos前后插入节点都很简单)
//就算pos是头节点 或者 是最后一个节点 都很容易实现
//不过pos是头节点用此方法相当于 尾插
//pos是phead->next则相当于 头插
void ListInsert(ListNode* pos, LTDataType x)
{
	assert(pos);

	ListNode* prev = pos->prev;
	ListNode* newnode = BuyListNode(x);

	prev->next = newnode;//双向
	newnode->prev = prev;

	newnode->next = pos;//双向
	pos->prev = newnode;
}

8、删除pos节点后的节点

对于两种情况,一种解决方案即可

数据结构——链表 (单向不带头不循环链表 与 双向带头循环链表)_第22张图片

void ListErase(ListNode* pos)
{
	assert(pos);

	ListNode* prev = pos->prev;
	ListNode* next = pos->next;
	
	prev->next = next;
	next->prev = prev;

	free(pos);
}

9、检查链表(空返回1,非空返回0)

int ListEmpty(ListNode* phead)
{
	assert(phead);
	return phead->next == phead ? 1 : 0;
}

10、计算链表有效值个数

int ListSize(ListNode* phead) 
{
	assert(phead);

	int size = 0;
	ListNode* cur = phead->next;
	while (cur != phead)
	{
		++size;
		cur = cur->next;
	}
	return size;
	
}

11、清空链表

void ListDestory(ListNode* phead)
{
	assert(phead);

	ListNode* cur = phead->next;
	while (cur != phead)
	{
		ListNode* next = cur->next;
		free(cur);
		cur = next;
	}

	free(phead);
	phead = NULL;//此处没用,需要传递二级指针,但是会破坏一致性
	//最后要置空头指针,需要在.c文件最后plist = NULL;置空
}

12、双向链表总结

只要拥有7、8函数表头,即可实现头插尾插,头删尾删

但是要注意phead节点的使用

//尾插节点
void ListPushBack(ListNode* phead, LTDataType x)
{

	ListInsert(phead, x);

}

//头插
void ListPushFront(ListNode* phead, LTDataType x)
{
	
	ListInsert(phead->next, x);
}

//尾删
void ListPopBack(ListNode* phead)
{
	
	ListErase(phead->prev);
}

//头删
void ListPopFront(ListNode* phead)
{

	ListErase(phead->next);

}


顺序表和链表的优缺点对比

(双向带头循环链表)

  • 顺序表优点:

1、按下标去进行随机访问

2、 CPU高速缓存命中率较高

  • 顺序表缺点:

1、 空间不够需要增容(一定程序的性能消耗),可能存在一定的空间浪费

2、 头部或中间插入删除数据,需要挪动数据,效率比较低->O(N)

  • 链表优点:

1、 按需申请内存,需要存一个数据,就申请一块内存,不存在浪费

2、 任意位置O(1) 时间内插入删除数据

  • 链表缺点:

1、 不支持下标的随机访问

总结: 这两数据结构是相辅相成的,互相弥补对方的缺点,需要用谁存数据,具体看场景

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