单链表:带头结点和不带头结点 总结

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/tonglin12138/article/details/88801460

写在前面:

       一直以来,带头结点和不带头节点的单链表的操作实现困扰着我。在没有理解和实现之前,光凭思考真的是很难理解清楚
1.两者之间代码的差异;
2.带不带头结点的区别;
3.带头结点之后,什么情况下形参必须传二级指针(或者一级指针的引用);

       所以,当我攻克掉这个难题之后,就准备一次性捋清楚这些点,一来以便自己回顾用;而来,也希望对陌生的有需要的你有相关的帮助。

       OK,先简单摆出代码:

不带头结点的单链表操作:

//SList.h
#pragma once

typedef int SLDataType ;

typedef struct SListNode
{
	int data ;
	struct SListNode* next ;
}SListNode ;

typedef struct SList
{
	struct SListNode* first ;
}SList ;

//初始化&销毁
void SListInit(SList* list) ;

//销毁
void SListDestroy(SList* list) ;

//头插
void SListPushFront(SList* list, SLDataType data) ;

//头删
void SListPopFront(SList* list) ;

//打印
void SListPrint(SList* list) ;

//尾插
void SListPushBack(SList* list, SLDataType data) ;

//尾删
void SListPopBack(SList* list) ;

//查找
SListNode* SListFind(SList* list, SLDataType data) ;

//在pos位置的节点后插入元素
void SListInsertAfter(SListNode* pos, SLDataType data) ;

//删除pos位置后的第一个节点
void SListEraseAfter(SListNode* pos) ;

//删除遇到的指定的第一个节点
void SListRemove(SList* list, SLDataType data) ;
//**************************************************
//SList.c
#include "SList.h"
#include <stdio.h>
#include <assert.h>
#include <stdlib.h>

//初始化
void SListInit(SList* list) 
{
	assert(list != NULL) ;
	list->first = NULL ;
}

//销毁
void SListDestroy(SList* list) 
{
	SListNode* next ;
	SListNode* cur ;
	for(cur=list->first; cur!=NULL; cur=next)
	{
		next = cur->next ;
		free(cur) ;
	}
	list->first = NULL ;
}

//头插
void SListPushFront(SList* list, SLDataType data) 
{
	SListNode* node = (SListNode*) malloc (sizeof(SListNode)) ;
	assert(node) ;
	node->data = data ;
	node->next = list->first ;
    //更新头指针的指向;
	list->first = node ;
}

//头删
void SListPopFront(SList* list) 
{
	SListNode* old_first = list->first ;
	assert(list) ; //有无链表
	assert(list->first != NULL) ; //有链表,链表里是否有元素,
	                              //若链表为空,则删除失败
	list->first = list->first->next ;
	free(old_first) ;
}

//打印
void SListPrint(SList* list) 
{
	SListNode* cur ;
	assert(list) ;
	for(cur=list->first; cur!=NULL; cur=cur->next)
	{
		printf("%d-->", cur->data) ;
	}
	printf("NULL\n") ;
}

//尾插
void SListPushBack(SList* list, SLDataType data)
{
	SListNode* node = (SListNode*) malloc (sizeof(SListNode)) ;
	SListNode* lastone = list->first ;
	assert(list != NULL) ;
	for( ; lastone->next != NULL; lastone=lastone->next)
	{}
	assert(node != NULL) ;
	node->data = data ;
	node->next = lastone ->next ;
	lastone->next = node ;
}

//尾删
void SListPopBack(SList* list)
{
	SListNode* cur ;
	SListNode* m ;
	assert(list != NULL) ;
	assert(list->first != NULL) ;
	if(list->first->next  == NULL)
	{
		//若链表为空,则尾删即为头删
		SListPopFront(list) ;
		return ;
	}
	for(cur=list->first; cur->next->next != NULL; cur=cur->next)
	{
	}//找到倒数第二个节点
    m = cur->next ;
	cur->next = m->next ;
	free(m) ;
}

//查找
SListNode* SListFind(SList* list, SLDataType data)
{
	SListNode* cur = list->first ;
	for( ; cur!=NULL; cur=cur->next)
	{
		if(cur->data == data) 
		{
			return cur ;
		}
	}
	return NULL ;
}

//在pos位置的节点后插入元素
void SListInsertAfter(SListNode* pos, SLDataType data) 
{
	SListNode* node = (SListNode*) malloc (sizeof(SListNode)) ;
	assert(node != NULL ) ;
	node->data = data ;
	node->next = pos->next ;
	pos->next = node ;
}

//删除pos位置后的第一个节点
void SListEraseAfter(SListNode* pos)
{
	SListNode* node = pos->next->next ;
	free(pos->next) ;
	pos->next = node ;
}

//删除遇到的指定的第一个节点
void SListRemove(SList* list, SLDataType data)
{
	SListNode* prev = NULL ;
	SListNode* cur = list->first ;
	while(cur != NULL && cur->data != data)
	{
		prev = cur ;
		cur = cur->next ;
	}
	//要删除的节点不存在
	if(cur == NULL )
	{
		return ;
	}
	//要删除的节点若就是第一个节点
	if(prev == NULL)
	{
		//即头删
		SListPopFront(list) ;
		return ;
	}
	prev->next = cur->next ;
	free(cur) ;
}

带头结点的单链表简单操作:

//带头结点的单链表的实现
#pragma once

#include<assert.h>
#include<stdio.h>
#include<stdlib.h>
typedef int DataType;

typedef struct ListNode{
	DataType _data;
	struct ListNode* next;

}Node,* PNode;


PNode buy_new_node(int x)
{
	PNode _node = (PNode)malloc(sizeof(Node));
	_node->_data = x;
	_node->next = NULL;
	return _node;

}

//初始化
void init(PNode head)
{

	PNode node = buy_new_node(0);
	head->next = node;

}
//头插
void push_front(PNode* head,DataType x)
{
	assert(*head != NULL);
	PNode _node = buy_new_node(x);
	_node->_data = x;
	_node->next = (*head)->next;
	(*head)->next = _node;

}


//头删
void pop_front(PNode* head)
{
	assert(*head != NULL);
	PNode tmp = (*head)->next;
	(*head)->next = (*head)->next->next;
	free(tmp);

}

//尾插
void push_back(PNode head,DataType x)
{
	assert(head != NULL);
	//r如果链表为空,那么就是进行头插
	if ((head->next) == NULL)
	{
		push_front( &head, x);
	}
	//链表不为空
	PNode cur = NULL;
	for (cur = head->next; cur->next != NULL; cur = cur->next)
	{}
	PNode _node = buy_new_node(x);
	_node->_data = x;
	_node->next = cur->next;
	cur->next = _node;

}


//尾删
void pop_back(PNode head)
{
	assert(head != NULL);
	//如果只有一个元素 ,那么进行头删
	if ((head->next->next) == NULL)
	{
		pop_front(&head);
	}
	//否则
	PNode cur = NULL;
	for (cur = head->next; cur->next->next != NULL; cur = cur->next)
	{}
	PNode tmp = cur->next;
	cur->next = tmp->next;
	free(tmp);



}
//pos位置之后插入  更改头插和头删的代码


//打印
void print(PNode head)
{
	assert(head != NULL);
	if (head->next == NULL)
		return;


	PNode cur = NULL;
	for (cur = head->next;cur->next!=NULL;cur=cur->next)
	{
		printf("%d--> ", cur->_data);
	}
	printf("NULL");

}

OK,先简单的展示出相关代码,暂时解决掉第一个问题。下来我们分析第二个问题:带不带头结点操作的区别又在哪里呢?

第一点:

不带头节点: p1->p2->p3 ->p1->p2->p3-> p1…
       我们先创建头指针,初始化的时候只需要将头指针赋NULL就可以了;大家想想,如果在这种情况下,我们进行头删操作,最后一步必须得做的事就是: 更新头指针的指向
或者换句话说,一旦对首元节点(头指针指向首元结点)进行操作,首尾工作必定得进行更新

带头节点:head-> p1->p2->p3 ->p1->p2->p3-> p1…
       与上边相反,每次进行涉及到首元结点的操作后,更新的过程就会很简单。我们不需要再去移动头指针,只需要固定的更新头结点中的指针域就可以了。

第二点:

再者,带头节点可以方便,快速的定位链表第1个节点。
比如循环链表的时候,删除p1 的时候:

head->next = p2;
p3->next = head->next; 
free(p1);

思路很清晰,链表开始的第1个节点现在就是head->next 即 p2

否则没有头节点:

p3->next = p1->next;
free(p1); 

会不会有一种链表第1个节点到底是哪个的感觉?

第三点:

不带头结点的单链表对于第一个节点的操作与其他节点不一样,需要特殊处理,这增加了程序的复杂性和出现bug的机会,因此,通常在单链表的开始结点之前附设一个头结点。

       
Ok,趁热打铁,最后我们分析带头结点后的传参二级指针(或一级指针的引用)问题。
这个问题一度困扰我很久,想明白之后其实也觉得没什么。
1.我们先分析什么时候需要形参是二级指针?
答: 在对带头结点的单链表进行涉及到头指针的操作,例如头插,头删等操作时,形参必须是二级指针。

2.为什么?
大家还记得最初学习的时候,我们应该都写过一个交换两个数字的swap()函数吧。我们形参如果是 int a ,int b;实参传swap(a,b),我们发现执行swap()之后,并没有达到交换的结果。原因就是在swap函数中形参是实参的一份临时拷贝,swap栈帧销毁之后,对应产生的临时变量也就销毁了。那么,我们是怎么解决的呢?

//我们形参这么去定义
swap(int *a,int* b);
...
...
//实参传的是两个数对应的地址
swap(&a,&b);

对。我们实参传递的是这两个int类型的地址。
我们想要修改int类型的变量,形参必须得传int类型的地址,形参用int ※接收;
那么同理,我们现在想要修改头结点中的指针域(指向首元结点),是node※类型。那么我们就理应传地址过去,形参对应的用 node※※接收。

node* head;
push_front(&head, 1);  //head取址,形参用二级指针接收

形参的类型是由传递的实参决定的。

到这里,三个问题都已经做出了相应的解答,希望可以对有需要的您有所帮助。也欢迎大家在评论区补充指正。

猜你喜欢

转载自blog.csdn.net/tonglin12138/article/details/88801460