单链表
单链表概念
1.链表的概念:链表其实是将一个个相同或者不同的数据用指针串联起来。
链表结构和火车相似。在淡季的时候,去外面旅游的人少,火车装载的车厢少。当节假日时,旅游的人数很多,每列火车就会多装载几节车厢。这些车厢是独立的,用不到的时候,可以拆卸,而不会影响剩余的车厢。如果你在车头,要到车尾,每个车厢之间通过某种方式建立联系,一个车厢一个车厢的走,才能到达车尾。在链表中,每节车厢都被称为“结点”(节点),每个结点中有我们要存储的数据,也有指向下一个结点的的指针。
2.与顺序表对比:顺序表每个数据之间的储存位置是连续的,而顺序表存储的位置不连。如果要对数据进行插入,顺序表还要挪动其它数据的位置,而链表在这方面就有很大的优势,它可以通过修改结点的指向来插入数据。
每个结点中的指针就是为了存储下一个结点的地址,通过这个就可以访问下一个结点。图中的数据可以是任意类型,任意个数据。每个结点都需要向堆区申请空间。
单链表的实现
单链表头文件包含
#pragma once
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
typedef int TYPEDATA;//定义数据类型
//创建一个节点
typedef struct NODE
{
TYPEDATA data;//看作是数据
struct SL* nextnode;//指向下一个结点的指针
}SL;void PushFront(SL** pphead, TYPEDATA data);//头插
void DelData(SL** pphead);//头删
void AddBack(SL** pphead, TYPEDATA data);//尾插
void DelBack(SL** pphead);//尾删void AddAny(SL** pphead, TYPEDATA data);//任意位置插入
void DelAny(SL** pphead, TYPEDATA data);//在任意位置删除void ModifyData(SL** pphead, TYPEDATA data);//修改已经存在的数据
SL* Find(SL** pphead, TYPEDATA data);//查找
void PrintData(SL** pphead);//打印
单链表各功能实现
#include"SQL.h"
//头插
void PushFront(SL** pphead, TYPEDATA data)
{
SL* newnode = (SL*)malloc(sizeof(SL));//先建立节点,
if (newnode == NULL) //
{
perror("malloc failed!");
return 1;
}
newnode->data = data;
newnode->nextnode = *pphead; //然后将这个节点中的指针指向目前的头节点
(*pphead) = newnode; //然后将只向头指针的指针指向新建立的节点
} //头指针—>节点1,新节点-->节点1,头指针-->新节点。就达成了:头指针—>新节点-->节点1。
//头删
void DelData(SL** pphead)
{
assert(*pphead);//断言,如果链表中没有节点,就终止执行
SL* newnode = *pphead;//建立指针指向头节点,
*pphead = (*pphead)->nextnode;//然后将指向头节点的指针指向下一个节点
free(newnode);//销毁头节点
newnode = NULL;//防止newnode变成野指针
}
//尾插
void AddBack(SL** pphead, TYPEDATA data)
{
if (*pphead == NULL)//如果没有结点,可以直接使用头插
{
PushFront(pphead, data);
}
else
{
SL* pretail = *pphead;
while (pretail->nextnode)//使pretail指向的该结点的下一个指针刚好是空指针
{
pretail = pretail->nextnode;
}
SL* newnode = (SL*)malloc(sizeof(SL));
if (newnode == NULL)
{
perror("malloc failed!");
return 1;
}
newnode->data = data;//使pretail所指的结点指向新创立的结点,然后新创立的结点指向空指针
pretail->nextnode = newnode;
newnode->nextnode = NULL;
}
}
//尾删
void DelBack(SL** pphead)//尾删的思路是:创建两个指针,第一个指针指向第一个结点,第二个指针指向第二个结点,然后使第一个指针指向第二个结点,第二个指针指向第三第三个结点,直到第二个指针的下一个结点是NULL
{
assert(*pphead);//如果没有结点,就终止程序
SL* backnode = (*pphead)->nextnode;//
SL* pre = *pphead;
while (backnode&&backnode->nextnode)//这里的思路是:若backnode指向的下一个结点为空就退出,刚好backnode会指向NULL前一个指针。
{
backnode = backnode->nextnode;
pre = pre->nextnode;
}
if (pre->nextnode==NULL)//这里出现了特殊情况:剩下最后一个结点时,不能要用这样的方法去消除最后一个结点
{
free(*pphead);
*pphead = NULL;//使指向头结点的指针指向NULL
return;
}
free(backnode);//循环出来后,backnode刚好指向最后一个结点
backnode = NULL;
pre->nextnode = NULL;//pre指向的是倒数第二个结点,然后使第二个结点中的指针指向NULL
}
//在某数据前添加结点
void AddAny(SL** pphead, TYPEDATA data)//在查找的数据的结点前添加数据
{
if (*pphead == NULL)//如果没有结点,可以直接使用头插
{
PushFront(pphead, data);
return;
}
if (Find(pphead, data)) //先看看有没有该数据,如果没有就退出
{
TYPEDATA n;
printf("输入你要添加的数据\n"); scanf("%d", &n);
SL* newnode = (SL*)malloc(sizeof(SL));//先建立节点
if (newnode == NULL)
{
perror("molloc failed!");
return;
}
newnode->data = n;
SL* find = Find(pphead, data);//接收要查找的数据前一个结点的地址
newnode->nextnode = find->nextnode;//先将新建立的结点指向含有目标数据的地址,再将该目标数据的前一个结点指向新建立的结点
find->nextnode = newnode;//如果不熟悉结构指针传递,建议画图,
}
else
return;
}
//删除已经存在的数据
void DelAny(SL** pphead, TYPEDATA data)
{
assert(*pphead);//如果没有结点,就终止程序
SL* pre = Find(pphead, data);
if (pre)//判断数据是否存在
{
SL* back = pre->nextnode;
pre->nextnode = back->nextnode;//将要删除的结点两边的结点连接起来
free(back);
back = NULL;
}
else
{
printf("要删除的数据不存在\n");
return;
}
}
//修改已经存在的数据
void ModifyData(SL** pphead, TYPEDATA data)
{
SL* check = *pphead;
while (check)
{
if (check->data == data)//让它循环到目标结点,然后对它进行修改
{
TYPEDATA n=0;
printf("请输入你要修改后的数据\n"); scanf("%d", &n);
check->data = n;
return;
}
check = check->nextnode;
}//如果循环退出,说明要修改的数据不存在
printf("没有你要修改的数据\n");
}
//查找
SL* Find(SL** pphead,TYPEDATA data)//返回要找的数据前一个结点的地址
{
assert(*pphead);//如果没有数据,就终止查找
SL* des = (*pphead)->nextnode;
SL* pre = *pphead;
while (des)//des指针指向下一个结点的地址,如果是NULL,说明没找到
{
if (des->data == data)
{
return pre;
}
des = des->nextnode;
pre = pre->nextnode;
}
printf("没找到\n");
return NULL;
}
void PrintData(SL** pphead)//打印
{
assert(*pphead);
SL* newnode = *pphead;
while (newnode)
{
printf("%d ", newnode->data);//打印的思路是:创建一个结点类型的指针每打印完一个,就指向下一个结点,直到指向NULL就退出
newnode=newnode->nextnode;
}
printf("\n");
}
力扣经典算法题
1.反转链表
方法:
各位亲们,这画图太耗时间了,咱就不画了,先送思路和代码吧,还请谅解。
代码实现:
typedef struct ListNode List;
struct ListNode* reverseList(struct ListNode* head) {
List*mid=head;
if(head==NULL)//如果没有结点,就返回头指针
{
return head;
}
List *front=head->next;
List*back=NULL;
while(mid)
{
//使中间的指针指向后面的指针
mid->next=back;
//然后使各指针向前移动,必须要从后面的指针开始移动,不然后面的其中一个指针会丢失结点地址,找不到下一个结点的位置
back=mid;
mid=front;
if(front!=NULL)//如果front指向空指针,就不能对它解引用
{
front=front->next;
}
}
return back;
}
2.移除链表元素
方法一:
typedef struct ListNode LS;
struct ListNode* removeElements(struct ListNode* head, int val) {
LS*front=head;
LS*des=NULL;
LS*back=NULL;
LS*newhead=NULL;
while(front)//这里front!=NULL
{
if(front->val==val)//这里总体思路是:如果front->val==val,那么就对该结点进行销毁
{
while(front&&front->val==val)//如果有几个相邻的结点front->val==val,那么就对这几个结点重复销毁
{
des=front;
front=front->next;
free(des);
des=NULL;
}//走出循环后,说明现在front->val!=val
}
//先让后面的指针先指向front,在让front走向下一个结点
if(back==NULL)//这里的判断不能直接写back->next=front,如果所有的数据都是val,那么就不能对空指针解引用
{
back=front;
}
else
{
back->next=front;
back=front;
}
if(newhead==NULL&&back!=NULL)//先将头结点指向空,然后再找出第一个符合条件的结点作为头结点赋newhead
{
newhead=back;
}
if(front)
{
front=front->next;
}
}
return newhead;
}
方法二:
typedef struct ListNode LS;
struct ListNode* removeElements(struct ListNode* head, int val) {
LS*newhead=NULL;
LS*tail=NULL;//
LS*front1=head;//遍历原链表
while(front1)
{
if(front1->val!=val)//判断front1所指向的结点的val是否符合题意,如果符合,就将该结点连接到新链表中
{
if(newhead==NULL)//刚进入循环,第一个符合条件的结点赋给头指针
{
newhead=tail=front1;//这里先将头指针
}
else
{
tail->next=front1;//先将该结点指向下一个结点,然后将指针指向下一个结点
tail=tail->next;
}
}
front1=front1->next;
}
if(tail)
{
tail->next=NULL;
}
return newhead;
}
总结
数据结构中,理解链表并不是很难。单链表应该要比顺序表好理解。当我们每次学习完一个知识点的时候,首先就要去做总结,然后通过刷题加深对知识的理解,然后再次总结。我们对知识的理解总是不断地加深,可能以前的观点到了现在就会有点不符合,这都是很正常的事。人与社会一样,每时每刻都在发生变化,进步还是倒退都取决于自己。磨练才能有一番成就!