1、链表的概念
存储数据的一种结构,不强制在内存中集中存储,元素分散存储。一个元素配备一个指针,指针指向后继元素。
定义:数据元素随机存储在内存中,通过指针维系数据之间“一对一”的逻辑关系。
核心:一个节点只能有一个后继节点。但是不代表一个节点只能有一个被指向。
不正确:
正确:
重要概念:节点和头结点,虚拟节点。
节点:由值和指向下一个节点的地址组成的独立单元。
头结点:单链表的第一个节点。可以进行链表的遍历。
虚拟节点:虚拟节点的指针指向head节点,val值不会被使用,可以被初始化为0或者-1。方便我们处理首节点,可用于链表的反转等。
2、链表的构造
首先我们要想进行构造链表,必须要明白链表的组成?正如我前面讲到的,链表是由节点组成的,那节点又是有什么组成的呢?
见上图,一个节点是由数据+指针组成的,数据就代表当前节点所存放的数据,而指针就指向下一个节点的指针。由此我们就可以得到链表的数据结构图:
创建节点:
// C语言
struct ListNode {
int val; //代表数据
struct ListNode *next; //代表指针
}
创建函数:
struct ListNode* initLink()
{
int i;
//1、创建头指针
struct ListNode* p = NULL;
//2、创建头结点
struct ListNode* temp = (struct ListNode*)malloc(sizeof(struct ListNode));
temp->val = 0;
temp->next = NULL;
//头指针指向头结点
p = temp;
//3、每创建一个结点,都令其直接前驱结点的指针指向它
for (i = 1; i < 5; i++)
{
//创建一个结点
struct ListNode* a = (struct ListNode*)malloc(sizeof(struct ListNode));
a->val = i;
a->next = NULL;
//每次 temp 指向的结点就是 a 的直接前驱结点
temp->next = a;
//temp指向下一个结点(也就是a),为下次添加结点做准备
temp = temp->next;
}
return p;
}
测试方法:
int main()
{
struct ListNode* p = NULL;
printf("初始化链表为:\n");
//创建链表{1,2,3,4}
p = initLink();
return 0;
}
3、链表的增删改查
不管对于什么数据结构,只要有数据的存储就一定会有数据的取出,而不管我们使用什么数据结构还是算法,对该数据结构的遍历、新增、删除都是至关重要的!!!
3.1链表的遍历
思路:利用链表的存储方式,以及定义来进行遍历。找到当前链表的头结点!!!
//打印链表
void printList(struct ListNode* p) {
struct ListNode* temp = p;//temp指针用来遍历链表
//只要temp指向结点的next值不是NULL,就执行输出语句。
while (temp) {
// struct ListNode* f = temp;//准备释放链表中的结点
printf("%d ", temp->val);
temp = temp->next;
// free(f);
}
printf("\n");
}
//获取链表的长度
int32_t getLength(struct ListNode* p) {
struct ListNode* temp = p;//temp指针用来遍历链表
int length=0;
//只要temp指向结点的next值不是NULL,就执行输出语句。
while (temp) {
// struct ListNode* f = temp;//准备释放链表中的结点
length++;
temp = temp->next;
// free(f);
}
return length;
}
测试方法:
int main() {
struct ListNode* p = NULL;
printf("create list: \t\n");
//创建链表{1,2,3,4}
p = initLink();
printList(p);
int length=getLength(p);
printf("list length: %d\n",length);
return 0;
}
3.2链表的插入
思路:单链表的插入和数组结构方式的插入大同小异。我们要明白插入的具体逻辑,只有逻辑清楚了,才能进行实施。
类型:头插法、中间插、尾插法。
方法:前面讲过链表的插入和数组差不多,那我们不妨回想一下数组元素是怎么插入的。假设我们数组存放的类型是整数型,那么它就是由很多整数组成的。那么我们首先要创造一个我们想要插入的整数,之后再找到我们想要插入的具体位置,在我们找到插入的位置后,我们需要将当前位置的数据保存下来,把我们插入的数据放进去。这样基本就完成了数组元素的插入。
接下来我们再回到正题!!是不是就很简单了。
既然链表是由节点组成的,那么我们进行链表插入的时候自然是先创建一个新的节点new,之后再利用链表的遍历找到我们想要插入的位置curr。那么我们想要插入到当前位置难么我们要创建一个临时节点temp指向当前插入的节点。此时的关系就是pre(curr的前驱结点)指向curr,temp指向curr,此时我们还需要做的就是将pre指向new,new指向temp的指向。
头插法:
中间插:
尾插法:
代码:
struct ListNode* insertNode(struct ListNode* head, struct ListNode* nodeInsert, int position) {
if (head == NULL) {
// 这里可以认为待插入的节点就是链表的头节点,也可以抛出不能插入的异常
return nodeInsert;
}
int size = getLength(head);
if (position > size + 1 || position < 1) {
printf("位置参数越界");
return head;
}
// 插入节点到头部
if (position == 1) {
nodeInsert->next = head;
head = nodeInsert;
return head;
}
struct ListNode* pNode = head;
int count = 1;
// 遍历链表,找到插入位置的前一个节点
while (count < position - 1) {
pNode = pNode->next;
count++;
}
nodeInsert->next = pNode->next;
pNode->next = nodeInsert;
return head;
}
void testInsert(){
struct ListNode* head = NULL;
struct ListNode* node = (struct ListNode*)malloc(sizeof(struct ListNode));
node->val=1;
// 插入第一个元素
head=insertNode(head,node,1);
printList(head);
// 插入第二个元素,因为前面至于一个元素,这里就是在尾部插入了
node = (struct ListNode*)malloc(sizeof(struct ListNode));
node->val=3;
head=insertNode(head,node,2);
printList(head);
// 插入第二个元素,后面有个三,所以这里就是中间位置插入了
node = (struct ListNode*)malloc(sizeof(struct ListNode));
node->val=2;
head=insertNode(head,node,2);
printList(head);
}
3.3链表的删除
类型:头删除、中间删、尾删除。
方法:前面讲过链表的删除和数组差不多,那我们不妨回想一下数组元素是怎么删除的。我们首先要根据我们想要删除的数找到数组中对应的位置,保存当前位置下一个位置的状态,将当前位置的前驱位置指向当前位置的下一个位置。
接下来我们再回到正题!!是不是就很简单了。
首先根据遍历,找到我们想要删除的节点的位置curr。将当前位置的前驱节点pre指向当前位置的next。
头删除:
中间删除:
尾删除:
代码:
struct ListNode* deleteNode(struct ListNode*head, int position) {
if (head == NULL) {
return NULL;
}
int size = getLength(head);
if (position > size || position < 1) {
printf("输入的参数有误\n");
return head;
}
if (position == 1) {
struct ListNode*curNode=head;
head= head->next;
free(curNode);
return head;
} else {
struct ListNode* cur = head;
int count = 1;
while (count < position - 1) {
cur = cur->next;
count++;
}
struct ListNode*tmp = cur->next;
cur->next = tmp->next;
free(tmp);
return head;
}
}
void testDelete(){
struct ListNode* p = NULL;
printf("create list: \t\n");
//创建链表0~9
p = initLink();
printList(p);
// 删除第一个元素0
p= deleteNode(p,1);
printList(p);
//删除中间元素
p= deleteNode(p,5);
printList(p);
//删除末尾元素9
p= deleteNode(p,8);
printList(p);
}
扩展:
如果链表是有序的,如何根据顺序进行链表的插入和删除?
和链表的插入和删除一样,正常链表的插入和删除,是遍历到想要插入或者想要删除的位置,之后再进行删除或者插入。有序的话还是同样的进行遍历,如果当前节点的值是小于(大于)插入的值而下一个节点的值大于插入的值就进行插入。
代码:
//添加节点
void AddStruct(struct NUM **head,int ipnum)
{
struct NUM *new; //保存新节点地址
struct NUM *temp; //当前地址
struct NUM *last; //上一节点地址
last = NULL;
new = (struct NUM *)malloc(sizeof(struct NUM));//创建节点
if(new == NULL)
{
printf("内存分配失败\n");
exit(1);
}
//printf("内存分配成功\n");
new->num = ipnum; //保存数据到新节点
if(*head != NULL)//不是空链表
{
temp = *head; //传入的是指针的地址即指针的指针,取值即可得到指针
//printf("不是空链表\n");
while(temp != NULL && ipnum > temp->num) //指针没有指到链表结尾且插入值大于当前节点值
{
last = temp; //保存插入位置前一节点地址,该节点的指针指向新节点
temp = temp->next; //指针往后走
}
if(ipnum <= (*head)->num) //如果输入的数值小于或等于第一个节点的值
{
new->next = *head; //新节点的指针指向插入前的第一个节点
*head = new; //头指针指向新节点 (相当于头插法)
}
else
{
last->next = new; //插入位置的前一节点的指针指向新节点
new->next = temp; //新节点的指针指向后一节点
}
}
else //是空链表
{
printf("是空链表\n");
*head = new;
new->next = NULL;
}
}
/*删除节点*/
void DeleteStruct(struct NUM **head,int ipnum)
{
struct NUM *temp;
struct NUM *last;
temp = *head;
last = NULL;
while(temp != NULL && ipnum != temp->num) //指针没有指到链表结尾且插入值不等于当前节点值
{
last = temp; //保存插入位置前一节点地址,该节点的指针指向新节点
temp = temp->next; //指针往后走
}
if(temp == NULL)
{
printf("找不到该节点!\n");
}
else
{
if(last == NULL) //该值在第一个节点(上面的while不执行)
{
*head = temp->next; //头指针指向后一节点
}
else
{
last->next = temp->next; //上一节点的指针指向后一节点
}
free(temp); //释放空间
printf("已删除该节点!\n");
}
}
双向链表的构造?双向链表插入和删除?
双向链表和单链表一样都是由节点组成的,而单链表的节点包括当前节点的值和指向下一节点的指针。双向链表比单链表多的唯一的就是前驱指针。它指向的是前一个节点。同时形成闭环。
哨兵位节点:双向链表 在只有一个哨兵位时,让它自己指向自己。哨兵位的 next
指向它自己的prev
,哨兵位的 prev
指向它自己的 next
。说白了就是一个特殊的环形链表。
构造:
typedef struct ListNode
{
LTDataType data; // 保存数据
struct ListNode* next; // 记录下一个节点的地址
struct ListNode* prev; // 记录上一个节点的地址
}LTNode;
尾插:
头插:
尾删:
头删:
// 初始化
LTNode* ListInit()
{
LTNode* phead = (LTNode*)malloc(sizeof(LTNode));
// 双向带头循环链表的prev指向next,next指向prev
// 但是这里只有一个节点,所以只能让它自己指向自己
if (phead == NULL)
{
perror("ListInit");
exit(-1);
}
phead->next = phead;
phead->prev = phead;
return phead;
}
// 创建新节点
LTNode* BuyListNode(LTDataType x)
{
LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
if (newnode == NULL)
{
perror("ListPushBack");
exit(-1);
}
newnode->data = x;
newnode->next = NULL;
newnode->prev = NULL;
return newnode;
}
// 尾插
void ListPushBack(LTNode* phead, LTDataType x)
{
assert(phead);// 一定不为空-->有哨兵位
LTNode* tail = phead->prev;// 尾就是prev,由于是双向循环链表,所以头的prev就是尾
LTNode* newnode = BuyListNode(x);
// phead tail newnode
tail->next = newnode;
newnode->prev = tail;
newnode->next = phead;
phead->prev = newnode;
}
// 头插
void ListPushFront(LTNode* phead, LTDataType x)
{
assert(phead);
LTNode* newnode = BuyListNode(x);
LTNode* next = phead->next;
phead->next = newnode;
newnode->prev = phead;
newnode->next = next;
next->prev = newnode;
}
// 尾删
void ListPopBack(LTNode* phead)
{
assert(phead);
assert(phead->next != phead);// 防止把哨兵位删掉
LTNode* tail = phead->prev;
LTNode* tailprev = tail->prev;
free(tail);
phead->prev = tailprev;
tailprev->next = phead;
}
//头删
void ListPopFront(LTNode* phead)
{
assert(phead);
assert(phead->next != phead);
LTNode* next = phead->next;
LTNode* nextNext = next->next;
phead->next = nextNext;
nextNext->prev = phead;
free(next);
}