数据结构入门——链表

1.链表的概念与结构

        在上回的顺序表中有提到,顺序表在物理上和逻辑上是连续的,而链表则是在逻辑上连续,在物理上并不连续。打个比方就是,建筑A前面有一个指路牌写着建筑B,向着指路牌的方向走可以找到建筑B,建筑B前也可以有一个指路牌指向下一个地点,也可以没有,但是建筑A不能直接走到下一个地点。

        建筑和指路牌共同组成链表的结点,指路牌则是一个指针,指向下一个位置的地址。相比于顺序表,链表灵活了不少,结点是随机分布的,但是在访问的时候不能任意访问,因为地址总是存在上一个结点中。

2.单链表的实现

        链表的基础结构如下,

typedef int SLTDataType;

typedef struct SListNode
{
	SLTDataType x;
    //指向下一个节点的指针
	struct SListNode* next;
}SLTNode;

       接下来咱将实现单链表的几个接口

2.1创建节点

SLTNode* SLTCreate(SLTDataType x)
{
    //开辟一块空间
	SLTNode* newnode = (SLTNode * )malloc(sizeof(SLTNode));
	if (newnode == NULL)
	{
		perror("create newnode fail!");
		return NULL;
	}
    //单个节点的下一个节点指向空
	newnode->next = NULL;
    //赋值
	newnode->x = x;
	return newnode;
}

2.2插入数据

        在进行尾插时,顺序表在有容量的时候直接插入,没有容量的时候就开辟空间;链表则是直接创建一个节点,不用判断容量。

//从列表尾部插入数据
void SLTPushBack(SLTNode** pphead, SLTDataType x)
{
    //创建一个新的结点
	SLTNode* newnode = SLTCreate(x);
    //如果传入的结点是空,则新节点变成头
	if (*pphead == NULL)
	{
		(*pphead) = newnode;
	}
	else
	{
		SLTNode* ptail = *pphead;
        //遍历访问找到最后一个节点,最后ptail指向最后一个节点
		while (ptail->next)
		{
			ptail = ptail->next;
		}
        //把ptail和新节点连接
		ptail->next = newnode;
	}
}

        在进行头插时,顺序表需要挪动数据,而链表只需要将新节点与头结点连接,新节点置为头结点。

void SLTPushFront(SLTNode** pphead, SLTDataType x)
{
	
	SLTNode*newnode = SLTCreate(x);
	if (*pphead == NULL)
	{
		*pphead = newnode;
	}
	else
	{
        //新节点指向头结点
		newnode->next = *pphead;
        //新节点进化成新节点
		*pphead = newnode;
	}
}

2.3删除数据

        尾删就看起来有点复杂了,毕竟顺序表只需要在size--.

//尾删
void SLTPopBack(SLTNode** pphead)
{
	assert(*pphead && pphead);
	SLTNode* ptail = *pphead;
	SLTNode* prev = *pphead;
    //遍历找尾,保留尾节点前一项
	while (ptail->next != NULL)
	{
		prev = ptail;
		ptail = ptail->next;
	}
    //释放尾节点
	free(ptail);
	ptail = NULL;
    //前一项置为空
	prev->next = NULL;
}

        与头插类似,改变头结点的指向

void SLTPopFront(SLTNode** pphead)
{
	assert(*pphead && pphead);
	SLTNode* next = *pphead;
    //保存下一个节点
	next = next->next;
    //释放头结点
	free(*pphead);
    //新头结点登基
	*pphead = next;
	next = NULL;
}

2.4查找+插入

        很经典的遍历查找.

SLTNode* SLTFind(SLTNode* phead, SLTDataType x)
{
	assert(phead);
	//经典遍历查找
	while (phead != NULL)
	{
		if (phead->x == x)
		{
			return phead;
		}
		phead = phead->next;
	}
	return phead;
}

        两钟种基于查找的插入,

//在指定数据位置之前插入数据
void SLTInsert(SLTNode** pphead, SLTNode* pos , SLTDataType x)
{
	assert(*pphead && pphead);
	assert(pos);
    //如果指定位置是头,则变成头插,因为遍历不可能找到头的前一项
	if (pos == *pphead)
	{
		SLTPushFront(pphead, x);
	}
    //否则遍历查找到pos的前一项
	else
	{
        //新节点
		SLTNode* newnode = SLTCreate(x);
		SLTNode* pcur = *pphead;
        //遍历找到pos前一项
		while (pcur->next != pos)
		{
			pcur = pcur->next;
		}
        //改变节点的指向
		newnode->next = pos;
		pcur->next = newnode;
	}
}

        两者相比较,前者需要遍历找到pos前一项,改变节点时不用考虑顺序;后者如果不遍历找到pos后一项,那么只需要注意节点的连接顺序,因为一旦pos节点先和新节点连接,pos将找不到下一个节点。

//在指定位置之后插入数据
void SLTInsertback(SLTNode** pphead, SLTNode* pos, SLTDataType x)
{
	assert(pphead && *pphead);
	assert(pos);
	
	SLTNode* newnode = SLTCreate(x);
    //先把新节点和pos的下一个节点连接,以防丢失
	newnode->next = pos->next;
    //pos节点和新节点连接
	pos->next = newnode;
}

2.5插入+删除

        指定位置删除,和指定位置前插入,有个点很类似,就是当指定位置为头的时候直接调用之前的删除/插入接口,而不为头的时候需要找到前一项,毕竟头不能在长个头罢。

//删除指定位置的结点
void SLTErase(SLTNode** pphead, SLTNode* pos)
{
	assert(*pphead && pphead);
	assert(pos);
    //如果指定位置在头,则头删
	if (*pphead == pos)
	{
		SLTPopFront(pphead);
	}
	else
	{
		SLTNode* pcur = *pphead;
        //找到指定位置的前一项
		while (pcur->next != pos)
		{
			pcur = pcur->next;
		}
        //将前一项和pos后一项连接,即使pos是最后一项
		pcur->next = pos->next;
        //释放pos
		free(pos);
		pos = NULL;
	}
}

        很好操作的后删~

//删除指定位置之后的结点
void SLTEraseAfter(SLTNode** pphead, SLTNode* pos)
{
	assert(pphead && *pphead);
	assert(pos->next);
    //保存删除的结点
	SLTNode* after = pos->next;
    //pos与after的下一项连接
	pos->next = after->next;
    //释放需要删除的结点
	free(after);
	after = NULL;
}

        2.6销毁

void SListDesTroy(SLTNode** pphead)
{
	assert(pphead && *pphead);
	SLTNode* pcur = *pphead;
	while (pcur != NULL)
	{
		pcur = pcur->next;
		free(*pphead);
		*pphead = pcur;
	}
	free(pcur);
}
//这么删
//  data    1      2       3
// 1     *pphead  pcur
// 2       free  *pphead  pcur
// 3              free   *pphead  pcur

3.基于单链表再实现通讯录

        顺序表都来了一手通讯录,单链表也得来一手!不得不说,在这一块,遍历用的相当频繁,那么int 遍历=0;

        基本操作来一手,

#define NAME_MAX 20
#define TEL_MAX 15
#define GENDER_MAX 5
#define ADD_MAX 100
#define AGE_MAX 4
//contact相当于创建节点的结构体类型
typedef SLTNode contact;
typedef struct PersonInfo
{
	char name[NAME_MAX];
	char tel[TEL_MAX];
	char age[AGE_MAX];
	char gender[GENDER_MAX];
	char address[ADD_MAX];
}peoinfo;

        简单修改一下类型,

typedef struct PersonInfo SLTDataType;
typedef struct SListNode
{
	SLTDataType* x;
	struct SListNode* next;
}SLTNode;

        接下来就是接口的实现.

3.1初始化通讯录

void InitContact(contact** con)
{
	//初始化传入的数据
	*con = (contact*)malloc(sizeof(contact));
	if (con == NULL)
	{
		perror("malloc fail");
		return;
	}
	(*con)->next = NULL;
	(*con)->x = (peoinfo*)malloc(sizeof(peoinfo));
	if ((*con)->x == NULL)
	{
		perror("malloc fail");
		free((*con)->x);
		(*con)->x = NULL;
		return;
	}
	// 这个name是一个char数组
	//((*con)->x)->name = 0;
	//这样就可以对这个char数组完成初始化赋值了
	strcpy(((*con)->x)->name, ""); 
	strcpy(((*con)->x)->address, "");
	strcpy(((*con)->x)->tel, "");
	strcpy(((*con)->x)->gender, "");
	strcpy(((*con)->x)->age, "");

}

3.2添加通讯录数据

        遍历x1

void AddContact(contact** con)
{
	//创建一个节点
	contact* info;
	InitContact(&info);
	printf("请输入联系人的姓名\n");
	scanf("%s", info->x->name);
	printf("请输入联系人的电话号码\n");
	scanf("%s", info->x->tel);
	printf("请输入联系人的年龄\n");
	scanf("%s", info->x->age);
	printf("请输入联系人的性别\n");
	scanf("%s", info->x->gender);
	printf("请输入联系人的住址\n");
	scanf("%s", info->x->address);
    //如果通讯录为空
	if ((*con)->x->name[0] == '\0')
	{
		*con = info;
	}
	else
	{
		//	尾插
		contact* tail = *con;
		while (tail->next != NULL)
			tail = tail->next;
		tail->next = info;
	}
}

3.3展示通讯录

        遍历x2

void ShowContact(contact* con)
{
	assert(con);
	contact* ptail = con;
	printf("%s %s %s %s %s\n", "姓名", "电话", "年龄", "性别", "地址");
	while (ptail != NULL)
	{
		printf("%3s %4s %5s %4s %3s",
             ptail->x->name, 
             ptail->x->tel, 
             ptail->x->age, 
             ptail->x->gender,
             ptail->x->address);
		printf("\n");
		ptail = ptail->next;
	}
}

3.4删除联系人数据

        遍历x3

//通过名字删除联系人
void DelContact(contact** con)
{
	assert(*con && con);
	printf("请输入想要删除的联系人名字\n");
	char name[NAME_MAX];
	scanf("%s", name);
    //指针一前一后
	contact* ptail = *con;
	contact* prev = ptail;
	while (ptail != NULL)
	{
		//要删除的如果是通讯录第一个数据
		if (strcmp((*con)->x->name, name) == 0)
		{
			(*con) = (*con)->next;
			free(prev);
			prev = NULL;
			ptail = *con;
			prev = ptail;
			printf("删除成功!\n");
			return;
		}
        //如果不是通讯录第一个数据
		if (strcmp(ptail->x->name, name) == 0)
		{
			prev->next = ptail->next;
			free(ptail);
			ptail = prev->next;
			printf("删除成功!\n");
			return;
		}
		else
		{
			prev = ptail;
			ptail = ptail->next;
		}
	}
	printf("联系人信息不存在\n");
}

3.5查找联系人

        遍历x4

void FindContact(contact* con)
{
	assert(con);
	printf("请输入想要寻找的联系人姓名\n");
	char name[NAME_MAX];
	scanf("%s", name);
	contact* ptail = con;
    //遍历查找
	while (ptail != NULL)
	{
		if (strcmp(ptail->x->name, name) == 0)
		{
			printf("%s %s %s %s %s\n", "姓名", "电话", "年龄", "性别", "地址");
			printf("%3s %4s %5s %4s %3s\n",
                     ptail->x->name,
                     ptail->x->tel,
                     ptail->x->age,
                     ptail->x->gender,
                     ptail->x->address);

			return ;
		}
		ptail = ptail->next;
	}
	printf("联系人信息不存在\n");
}

3.6修改联系人信息

        遍历x5

void ModifyContact(contact** con)
{
	assert(*con && con);
	printf("请输入想要修改的联系人姓名\n");
	char name[NAME_MAX];
	scanf("%s", name);
	contact* ptail = *con;
    //通过ptail遍历循环找到再修改,不改变*con
	while (ptail != NULL)
	{
		if (strcmp(ptail->x->name, name) == 0)
		{
			printf("请输入要修改的联系人的姓名\n");
			scanf("%s", ptail->x->name);
			printf("请输入要修改的联系人的电话号码\n");
			scanf("%s", ptail->x->tel);
			printf("请输入要修改的联系人的年龄\n");
			scanf("%s", ptail->x->age);
			printf("请输入要修改的联系人的性别\n");
			scanf("%s", ptail->x->gender);
			printf("请输入要修改的联系人的住址\n");
			scanf("%s", ptail->x->address);
			return;
		}
		ptail = ptail->next;
	}
		printf("联系人信息不存在\n");
}

3.7销毁通讯录

        遍历x7

void DestroyContact(contact** con)
{
	assert(*con && con);

	contact* prev = *con;
	contact* ptail = *con;
	while (ptail != NULL)
	{
		ptail = ptail->next;
        //释放通讯录中的结构体指针
		free(prev->x);
		free(prev);
		prev = ptail;
	}
}

        接口摆在这里,至于动起来,还请参见之前的文章。

4.链表的分类

        链表其实一共有八种,也就是1、2、3中任选一个总共有八种情况,最常用的两种为单链表和双向链表。

        单向就是链表只能通过上一个结点找到下一个节点,不能找到上一个结点;双向则既能找到下一个节点也能拐弯回头找到上一个结点。

        循环就是看链表尾节点指向的是否为链表上的任一结点,如果指向空则为不循环.

        在上面咱实现的是单链表,即不带头单向不循环链表。可咱在单链表部分有提到过头结点,为什么说单链表无头呢?

        实际上,单链表中的头不是真正意义上的头,只是为了称呼上的方便,而真正的头中不存放数据,不会因为节点的变动而消失,一直在那里。同时这个头还有一个别称——哨兵位。

        双向链表默认是指双向循环带头链表,第一个节点的前一项指向头,最后一个节点的后一项也指向头,如果不带头则指向空。

5.双向链表的实现

        既然双向链表也是常用的链表之一,那么咱也浅实现一下来对比单链表。

        结构上看,双向链表比单链表要多一个指向上一个位置的结点。

typedef int LTDataType;

typedef struct ListNode
{
	LTDataType x;
	struct ListNode* next;
	struct ListNode* prev;
}LTNode;

5.1初始化头结点

        相比于单链表,双向链表需要先初始化一个头结点,对于双向链表来说,空链表指只有一个头结点,而单链表没有节点。

//创建一个节点
LTNode* CreateNode(LTDataType x)
{
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
	if (newnode == NULL)
	{
		perror("create is failed");
		return;
	}
	newnode->x = x;
}
//初始化链表
LTNode* LTInit()
{
	//创建一个头结点
	LTNode* phead = CreateNode(0);
	phead->next = phead->prev = phead;
	return phead;
}

5.2插入数据

        在进行尾插时,最好控制的就是新节点的指向,所以放在第一步进行。之后就是改变尾节点d3的下一项和头结点的上一项,这里需要讲究顺序,需要先改变d3指向new(因为可以通过head找到尾节点),然后再让head的前一项指向new

//尾插
void LTPushBack(LTNode* phead,LTDataType x)
{
	assert(phead);
	LTNode*newnode = CreateNode(x);
    //先处理新节点的前后指向
	newnode->next = phead;
	newnode->prev = phead->prev;
    //原来的尾节点指向新节点
	phead->prev->next = newnode;
    //头结点的上一项指向新节点
	phead->prev = newnode;
}

        头插时,需要注意是插入在头结点后面,第一个节点之前。操作同上,先改变新节点的前后指向,再将d1指向new,head指向new(当然如果保存了第一个节点的话就不需要考虑顺序了)

void LTPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* newnode = CreateNode(x);
	newnode->next = phead->next;
	newnode->prev = phead;

	phead->next->prev = newnode;
	phead->next = newnode;
}

5.3判断链表是否为空

        双向链表为空时,只有一个头结点,也就是头结点的前后指针都指向空。对于单链表,判空似乎没什么意义,因为当链表为空时,assert就会报错。

bool LTEmpty(LTNode* phead)
{
	assert(phead);
	if (phead->next == phead->prev)
		return true;
	else
		return false;
}

5.4删除数据

        尾删时,先保存要删除的结点,然后将头节点和新的尾节点连接,最后释放节点。

void LTPopBack(LTNode* phead)
{
    //没有初始化链表和空链表时报错
	assert(phead&&phead->next != phead);
    //保留要删除的结点
	LTNode* del = phead->prev;
    //头的前指针指向删除节点的前一项,也就是新的尾节点
	phead->prev = del->prev;
    //新尾节点指向头结点
	del->prev->next = phead;
	free(del);
	del=NULL;
}

        头删的原理和尾删的原理相同,

void LTPopFront(LTNode* phead)
{
	assert(phead && phead->next != phead);
	LTNode* del = phead->next;
	phead->next = del->next;
	del->prev = phead;
	free(del);
}

5.5查找数据+插入/删除

        在查找这一块,双向链表也是遍历查找。

LTNode* LTFind(LTNode* phead, LTDataType x)
{
	assert(phead);
    //创建一个指针指向第一个节点
	LTNode* pcur = phead->next;
    //遍历查找
	while (pcur != phead)
	{
		if (pcur->x == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}
	return pcur;
}

        在指定位置之后插入数据时,因为这是循环链表,所以可以把pos当做哨兵位,于是就相当于在头插。

void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);
	LTNode* newnode = CreateNode(x);
	newnode->prev = pos;
	newnode->next = pos->next;

	pos->next->prev = newnode;
	pos->next = newnode;
}

        删除指定位置的数据时,pos已知,只需要通过pos改变d1和d3的指向,d1、d3就能连接。

void LTErase(LTNode* pos)
{
	assert(pos);
	pos->prev->next = pos->next;
	pos->next->prev = pos->prev;
	free(pos);
	pos = NULL;
}

5.6销毁链表

        销毁链表时从尾节点开始删除,就相当于尾删,尾删之后再保留新的尾节点进行删除,遍历删除,直到只剩下哨兵位。

void LTDestroy(LTNode* phead)
{
	assert(phead);
    //保留尾节点
	LTNode* del = phead->prev;
    //遍历删除
	while (del != phead)
	{
        //相当于w尾删
		del->prev->next = phead;
		phead->prev = del->prev;
		free(del);
		del = phead->prev;
	}
	free(phead);
    phead=NULL;
}

5.7双向链表和单链表的比较

        双向链表比单链复杂,一半用于单独储存数据,而且在实现时,遍历的次数少,时间复杂度低,使用代码的时候有很多优势。

        单向链表结构简单,一般不会用于单独储存数据,实际中更多是作为其他数据结构的子结构,而且这种结构在笔试面试中比较常见。

评论 3
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值