一、概念与结构

如图所示:双向链表是由一个一个的结点组成,这里的结点有三个组成部分:


struct ListNode
{
	int data;
	struct ListNode* next;//指向后一个结点的指针
	struct ListNode* prev;//指向前一个结点的指针
};

注意:这里的“带头”跟前面我们说的“头结点”是两个概念,实际前面的在单链表阶段称呼不严谨,但是为了同学们更好的理解就直接称为单链表的头结点。

带头链表里的头结点,实际为“哨兵位”,哨兵位结点不存储任何有效元素,只是站在这里“放哨的”

双向链表为空:他还是要满足带头、双向、循环这几个特点,实际上他只有一个哨兵位指针,他的next指针指向自己,prev指针也指向自己。

二、实现双向链表

2.1、List.h
#pragma once

#define _CRT_SECURE_NO_WARNINGS

#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include<stdbool.h>

//定义双向链表的结构
typedef int LTDataType;
typedef struct ListNode {
	LTDataType data;
	struct ListNode* next;//指向下一个结点的指针
	struct ListNode* prev;//指向前一个结点的指针
}LTNode;

//双向链表的初始化
//要改变头结点plist就要传入二级指针否则形参改变不了实参
void LTInit(LTNode** pphead);

//尾插
//在双向链表中,增删改查都不会改变哨兵位结点
void LTPushBack(LTNode* phead, LTDataType x);

//头插
//注意这里的头插是将newnode放到head之后,node1之前
//如果只是单纯的放在head前面那就和尾插无异了
void LTPushFront(LTNode* phead, LTDataType x);

//判断链表是否为空
bool LTEmpty(LTNode* phead);

//打印双向链表
//只需要打印node1、node2、node3...不需要打印phead这个哨兵位
void LTPrint(LTNode* phead);

//尾删
void LTPopBack(LTNode* phead);

//头删
//删除的是head后面的结点,并不是删除哨兵位
void LTPopFront(LTNode* phead);

//查找
LTNode* LTFind(LTNode* phead, LTDataType x);

//任意位置的插入
//双向链表我们知道其中一个的位置就可以找到任意位置的数据
void LTInsert(LTNode* pos, LTDataType x);

//在指定位置之前插入数据
void LTInsertFront(LTNode* pos, LTDataType x);

//删除pos位置的结点
void LTErase(LTNode* pos);

//销毁双向链表
//销毁的实际也是改变,要使用二级指针
void LTDesTroy(LTNode** pphead);
2.2、List.c
#include"List.h"

//申请一个新结点
LTNode* LTBuyNode(LTDataType x)
{
	LTNode* newnode = (LTNode*)malloc(sizeof(LTNode));
	if (newnode == NULL)
	{
		perror("malloc fail!");
		exit(1);
	}
	newnode->data = x;
	newnode->next = newnode->prev = newnode;

	return newnode;
}

//初始化双向链表
void LTInit(LTNode** pphead)
{
	*pphead = (LTNode*)malloc(sizeof(LTNode));
	if (*pphead == NULL)
	{
		perror("malloc fail!");
		exit(1);
	}
	(*pphead)->data = -1;//我们可以随意的先赋个值,不使用
	(*pphead)->next = (*pphead)->prev = *pphead;
	//现在就是一个空的双向链表

	//等价代码: *pphead = LTBuyNode(-1);
}

//尾插
void LTPushBack(LTNode* phead, LTDataType x)
{
	assert(phead);
	LTNode* newnode = LTBuyNode(x);
	//phead phead->prev newnode
	newnode->prev = phead->prev;
	newnode->next = phead;

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

//头插
void LTPushFront(LTNode* phead, LTDataType x)
{
	assert(phead);

	LTNode* newnode = LTBuyNode(x);
	//phead newnode phead->next
	newnode->next = phead->next;
	newnode->prev = phead;

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

//判断链表是否为空
bool LTEmpty(LTNode* phead)
{
	assert(phead);
	//一个双向链表如果他指向自己那就是空链表
	return phead->next == phead;
}

//打印双向链表
void LTPrint(LTNode* phead)
{
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
	    printf("%d -> ", pcur->data);
		pcur = pcur->next;
	}
	printf("\n");
}

//尾删
void LTPopBack(LTNode* phead)
{
	//双向链表不能为空
	assert(!LTEmpty(phead));

	LTNode* del = phead->prev;
	del->prev->next = phead;
	phead->prev = del->prev;

	free(del);
	del = NULL;
}

//头删
void LTPopFront(LTNode* phead)
{
	assert(!LTEmpty(phead));

	LTNode* del = phead->next;
	del->next->prev = phead;
	phead->next = del->next;

	free(del);
	del = NULL;
}

//查找
LTNode* LTFind(LTNode* phead, LTDataType x)
{
	assert(phead);
	//双向链表中的头结点是哨兵位后面的结点
	LTNode* pcur = phead->next;
	while (pcur != phead)
	{
		if (pcur->data == x)
		{
			return pcur;
		}
		pcur = pcur->next;
	}
	//未找到
	return NULL;
}

//任意位置的插入
//这里我们写在pos之后插入数据
void LTInsert(LTNode* pos, LTDataType x)
{
	assert(pos);

	LTNode* newnode = LTBuyNode(x);
	//pos newnode pos->next
	newnode->prev = pos;
	newnode->next = pos->next;

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

//在指定位置之前插入数据
void LTInsertFront(LTNode* pos, LTDataType x)
{
	assert(pos);

	LTNode* newnode = LTBuyNode(x);
	
	newnode->next = pos;
	newnode->prev = pos->prev;

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

//删除pos位置的结点
void LTErase(LTNode* pos)
{
	assert(pos);
	//pos->prev pos pos->next

	pos->prev->next = pos->next;
	pos->next->prev = pos->prev;

	free(pos);
	pos = NULL;
}

//销毁双向链表
void LTDesTroy(LTNode** pphead)
{
	LTNode* pcur = (*pphead)->next;
	while (pcur != *pphead)
	{
		LTNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	//销毁头结点
	free(*pphead);
	*pphead = NULL;
}

这里为了保持接口的一致性,我们可以改一下初始化和销毁的代码方便我们后期更好的使用和记忆

//初始化2
LTNode* LTInit2()
{
	LTNode* phead = LTBuyNode(-1);
	return phead;
}

//销毁2
void LTDesTroy2(LTNode* phead)
{
	LTNode* pcur = (phead)->next;
	while (pcur != phead)
	{
		LTNode* next = pcur->next;
		free(pcur);
		pcur = next;
	}
	//销毁头结点
	free(phead);
	phead = NULL;
}
2.3、test.c
#include"List.h"

void test01()
{
	LTNode* plist = NULL;
	LTInit(&plist);

	//尾插
	LTPushBack(plist, 1);
	LTPushBack(plist, 2);
	LTPushBack(plist, 3);
	LTPushBack(plist, 4);

	//头插
	/*LTPushFront(plist, 1);
	LTPushFront(plist, 2);
	LTPushFront(plist, 3);
	LTPushFront(plist, 4);*/

	//尾删
	/*LTPopBack(plist);
	LTPopBack(plist);
	LTPopBack(plist);
	LTPopBack(plist);
	LTPrint(plist);*/

	//头删
	/*LTPopFront(plist);
	LTPopFront(plist);
	LTPopFront(plist);
	LTPopFront(plist);
	LTPrint(plist);*/

	//查找
	LTNode* pos = LTFind(plist, 2);
	/*if (pos)
	{
		printf("找到了!\n");
	}
	else
	{
		printf("未找到!\n");
	}*/

	//在pos之后插入数据
	//LTInsert(pos, 416);
	//LTPrint(plist);

	//在pos之前插入数据
	LTInsertFront(pos, 416);
	LTPrint(plist);


	//删除pos位置的数据
	LTErase(pos);
	LTPrint(plist);

	//销毁
	LTDesTroy(&plist);

}

int main()
{
	test01();
	return 0;
}

我们在实现链表的时候要注意的是:我们指向的是结点的元素,而不是他的prev和next,这些指针只是为了帮助我们更好的去找到我们想要的位置。

三、顺序表与链表的分析

四、单链表加餐习题

4.1、链表分割

https://www.nowcoder.com/practice/0e27e0b064de4eacac178676ef9c9d70

思路:创建俩个链表(小链表、大链表),遍历原链表,小的尾插到小链表中,大的尾插到大链表中,大链表和小链表首尾相连

下面给出图解:

/*
struct ListNode {
    int val;
    struct ListNode *next;
    ListNode(int x) : val(x), next(NULL) {}
};*/
class Partition {
  public:
    ListNode* partition(ListNode* pHead, int x) {
        //创建俩个带头空链表
        ListNode* lesshead, *lesstail;
        lesshead = lesstail = (ListNode*)malloc(sizeof(ListNode));
        ListNode* greaterhead, *greatertail;
        greaterhead = greatertail = (ListNode*)malloc(sizeof(ListNode));

        ListNode* pcur = pHead;
        while (pcur) {
            if (pcur->val < x) {
                lesstail->next = pcur;
                lesstail = lesstail->next;
            } else {
                greatertail->next = pcur;
                greatertail = greatertail->next;
            }
            pcur = pcur->next;
        }
        //大链表尾结点的next指针要置为NULL(避免死循环)
        greatertail->next = NULL;
        //大小链表首尾相连
        lesstail->next = greaterhead->next;
        ListNode* ret = lesshead->next;
        free(lesshead);
        free(greaterhead);
        return ret;
    }
};
4.2、随机链表的复制(难)

https://leetcode.cn/problems/copy-list-with-random-pointer/description/

在写这个题目之前我们需要先了解一个概念:

浅拷贝:只复制对象的 “表层”,若对象内部包含其他引用类型数据(如数组、对象),则仅复制其引用地址,新旧对象共享这些内部数据。

深拷贝:完全复制对象的 “所有层级”,包括内部嵌套的引用类型数据,新旧对象是完全独立的,修改一方不会影响另一方。

在链表中解释就是:
浅拷贝:值拷贝

在链表中,深拷贝不仅会复制每个节点存储的 “值”,还会为每个原节点创建一个全新的对应节点,并重新建立节点间的指针(或引用)关系,最终得到一个与原链表结构完全相同但内存地址完全独立的新链表。

简单说,浅拷贝的链表节点只是 “值的副本”,但节点间的连接可能仍依赖原链表;深拷贝则是 “节点和连接的完整复刻”,修改新链表的任何节点或连接,都不会影响原链表。

思路:
1)在原链表的基础上拷贝结点

2)置random指针

3)断开新旧链表

/**
 * Definition for a Node.
 * struct Node {
 *     int val;
 *     struct Node *next;
 *     struct Node *random;
 * };
 */
typedef struct Node Node;
Node* buyNode(int x)
{
    Node* newnode = (Node*)malloc(sizeof(Node));
    newnode->val = x;
    newnode->next = newnode->random = NULL;

    return newnode;
}
void AddNode(Node* head)
{
    Node*pcur = head;
    while(pcur)
    {
        Node* newnode = buyNode(pcur->val);
        Node* next = pcur->next;
        newnode->next = next;
        pcur->next = newnode;
        pcur = next;
    }
}
void setRandom(Node* head)
{
    Node* pcur = head;
    while(pcur)
    {
        Node* copy = pcur->next;
        if(pcur->random)
            copy->random = pcur->random->next;
        pcur = copy->next;
    }
}
struct Node* copyRandomList(struct Node* head) {
	if(head == NULL)
    {
        return head;
    }
    //在原链表基础上拷贝结点并插入在原链表中
    AddNode(head);
    //设置random
    setRandom(head);
    //断开新链表
    Node* pcur = head;
    Node* copyHead,*copyTail;
    copyHead =copyTail = pcur->next;
    while(copyTail->next)
    {
        pcur = copyTail->next;
        copyTail->next = pcur->next;
        copyTail = copyTail->next;
    }
    return copyHead;
}

更多链表算法刷题入口:

牛客网:https://www.nowcoder.com/exam/oj

LeetCode:https://leetcode.cn/problems/copy-list-with-random-pointer/description/

记住该记住的,忘记该忘记的。改变能改变的,接受不能改变的
                                                                                                                               --《麦田守望者》

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐