【数据结构】详细剖析链表,带你实现单链表,双向链表,附链表编程练习题

目录

一. 链表

1. 链表的概念及结构

2. 单链表的实现

2.1 单链表节点结构

2.2 动态申请一个节点

2.3 单链表打印

2.4 单链表尾插

2.5 单链表头插

2.6 单链表尾删

2.7 单链表头删

2.8 单链表查找 

2.9 单链表在pos后一位插入x

2.10 单链表删除pos后一位的值

2.11 单链表销毁 

3. 链表的分类 

3.1 单向或双向

3.2 带头或不带头

3.3 循环或不循环

3.4 最常用 

4. 带头双向循环链表的实现

4.0 节点结构

4.1 创建节点

4.2 双向链表销毁

4.3 双向链表打印 

4.4 双向链表尾插

4.5 双向链表尾删

4.6 双向链表头插

4.7 双向链表头删

4.8 双向链表查找

4.9 双向链表在pos的前面进行插入

4.10 双向链表删除pos位置的节点

5. 链表编程练习题

5.1 移除链表元素

5.2 链表的中间结点

5.3 合并两个有序链表

5.4 反转链表 

5.5 链表分割

5.6 相交链表

5.7 环形链表 

5.8 环形链表返回环节点

5.9 随机链表的复制 

二. 顺序表和链表的区别 


一. 链表

1. 链表的概念及结构

概念:链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的 。

2. 单链表的实现

2.1 单链表节点结构

1. 使用typedef重命名数据类型是为了方便类型的更改。

2. 结构包含存放的数据和指向下一个节点的地址。

typedef int SLDataType;
typedef struct SingleListNode
{
	SLDataType data;
	struct SingleListNode* next;
}SLNode;

2.2 动态申请一个节点

1. 使用malloc申请一块节点空间。

2. 将节点内容初始化。 

SLNode* BuySLNode(SLDataType x)
{
	SLNode* newnode = (SLNode*)malloc(sizeof(SLNode));
	if (newnode == NULL)
	{
		perror("malloc");
		return NULL;
	}
	newnode->data = x;
	newnode->next = NULL;

	return newnode;
}

2.3 单链表打印

1. 通过获取下一个节点地址进行遍历并打印数据。

2. 遇到空节点停下。

void SLPrint(SLNode* plist)
{
	SLNode* cur = plist;
	while (cur)
	{
		printf("%d->", cur->data);
		cur = cur->next;
	}
	printf("NULL\n");
}

2.4 单链表尾插

1. pplist不可能为空,所以加断言。

2. 无节点情况:直接将新节点地址给头指针。

3. 有节点情况:将最后一个节点连接新节点。

void SLPushBack(SLNode** pplist, SLDataType x)
{
	assert(pplist);

	SLNode* newnode = BuySLNode(x);
	if (*pplist == NULL) *pplist = newnode; //无节点
	else                                    //有节点
	{
		SLNode* cur = *pplist;
		while (cur->next != NULL) cur = cur->next;
		cur->next = newnode;
	}
}

2.5 单链表头插

1. 先将新节点连接第一个结点,再将头指针连接新节点。   

void SLPushFront(SLNode** pplist, SLDataType x)
{
	assert(pplist);

	SLNode* newnode = BuySLNode(x);
	newnode->next = *pplist;
	*pplist = newnode;
}

2.6 单链表尾删

1. 单节点情况:释放节点然后置空。

2. 多节点情况:利用倒数第二个节点,释放倒数第一个节点并置空。

void SLPopBack(SLNode** pplist)
{
	assert(pplist && *pplist);

	SLNode* cur = *pplist;
	if (cur->next == NULL) //单节点
	{
		free(cur);
		*pplist = NULL;
	}
	else                   //多节点
	{
		while (cur->next->next != NULL) cur = cur->next;
		free(cur->next);
		cur->next = NULL;
	}
}

2.7 单链表头删

1. *pplist不能为空,因为空节点不用删。

2. 将头指针指向第二个节点,释放第一个节点。

void SLPopFront(SLNode** pplist)
{
	assert(pplist && *pplist);

	SLNode* del = *pplist;
	*pplist = del->next;
	free(del);
}

2.8 单链表查找 

1. 遍历一遍,比较数据。

SLNode* SLFind(SLNode* plist, SLDataType x)
{
	SLNode* cur = plist;
	while (cur)
	{
		if (cur->data == x) return cur;
		cur = cur->next;
	}

	return NULL;
}

2.9 单链表在pos后一位插入x

1. 先将新节点连接pos的后一个节点,再将pos连接新节点。

void SLInsertAfter(SLNode* pos, SLDataType x)
{
	assert(pos);

	SLNode* newnode = BuySLNode(x);
	newnode->next = pos->next;
	pos->next = newnode;
}

2.10 单链表删除pos后一位的值

1. 空节点不用删。

2. 将pos和pos后面第二个节点连接,释放pos后面第一个节点。

void SLEraseAfter(SLNode* pos)
{
	assert(pos);

	SLNode* del = pos->next;
	pos->next = del->next;
	free(del);
}

2.11 单链表销毁 

1. 遍历链表释放每个节点。

void SLDestroy(SLNode** pplist)
{
	assert(pplist);

	while (*pplist)
	{
		SLNode* cur = *pplist;
		*pplist = (*pplist)->next;
		free(cur);
	}
}

完整代码: SingleList/SingleList · 林宇恒/DataStructure - 码云 - 开源中国 (gitee.com)

3. 链表的分类 

链表可以分三类,这三类组合起来一共有8种结构。

3.1 单向或双向

3.2 带头或不带头

3.3 循环或不循环

3.4 最常用 

虽然有这么多的链表的结构,但是我们实际中最常用还是两种结构:

1. 无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。

2. 带头双向循环链表:结构最复杂,一般用在单独存储数据。实际中使用的链表数据结构,都是带头双向循环链表。

4. 带头双向循环链表的实现

4.0 节点结构

1. 两个指针,指向前面和后面。

typedef int ListDataType;
typedef struct ListNode
{
	ListDataType data;
	struct ListNode* prev;
	struct ListNode* next;
}ListNode;

4.1 创建节点

1. 这里可以创建哨兵位节点,也可以是其他新节点。

2. 通过malloc申请一块节点空间,将两个指针指向自己。

ListNode* ListCreate()
{
	ListNode* head = (ListNode*)malloc(sizeof(ListNode));
	if (head == NULL)
	{
		perror("malloc");
		return NULL;
	}
	head->prev = head;
	head->next = head;

	return head;
}

4.2 双向链表销毁

1. 先获取有效节点也就是哨兵位下一个节点,遍历释放空间,直到循环遇到哨兵位停下。

2. 最后把哨兵位也释放。

void ListDestory(ListNode* plist)
{
	assert(plist);

	ListNode* cur = plist->next;
	while (cur != plist)
	{
		ListNode* next = cur->next;
		free(cur);
		cur = next;
	}
	free(plist);
}

4.3 双向链表打印 

1. 获取哨兵位的下一个节点,遍历打印,直到循环遇到哨兵位停下。

void ListPrint(ListNode* plist)
{
	assert(plist);

	ListNode* cur = plist->next;
	printf("head<==>");
	while (cur != plist)
	{
		printf("%d<==>", cur->data);
		cur = cur->next;
	}
	printf("head\n");
}

4.4 双向链表尾插

1. 获取最后一个节点,将新节点和最后一个节点连接,再将新节点和哨兵位连接。

void ListPushBack(ListNode* plist, ListDataType x)
{
	assert(plist);

	ListNode* newnode = ListCreate();
	newnode->data = x;

	ListNode* tail = plist->prev;
	tail->next = newnode;
	newnode->prev = tail;
	newnode->next = plist;
	plist->prev = newnode;
}

4.5 双向链表尾删

1. 只有哨兵位时不允许删。

2. 获取最后一个节点和倒数第二个节点,将倒数第二个节点和哨兵位连接,释放最后一个节点。

void ListPopBack(ListNode* plist)
{
	assert(plist);
	assert(plist->next);

	ListNode* tail = plist->prev;
	ListNode* prev = tail->prev;
	prev->next = plist;
	plist->prev = prev;
	free(tail);
}

4.6 双向链表头插

1. 获取哨兵位的下一个节点,新节点和哨兵位连接,新节点再和哨兵位的下一个节点连接。

void ListPushFront(ListNode* plist, ListDataType x)
{
	assert(plist);

	ListNode* newnode = ListCreate();
	newnode->data = x;

	ListNode* cur = plist->next;
	plist->next = newnode;
	newnode->prev = plist;
	newnode->next = cur;
	cur->prev = newnode;
}

4.7 双向链表头删

1. 只有哨兵位时不允许删。

2. 获取第一个有效节点和第二个有效节点,将哨兵位与第二个有效节点连接,释放第一个有效节点。

void ListPopFront(ListNode* plist)
{
	assert(plist && plist->next);

	ListNode* cur = plist->next;
	ListNode* next = cur->next;
	plist->next = next;
	next->prev = plist;
	free(cur);
}

4.8 双向链表查找

1. 遍历有效节点,比较数据。

ListNode* ListFind(ListNode* plist, ListDataType x)
{
	assert(plist);

	ListNode* cur = plist->next;
	while (cur != plist)
	{
		if (cur->data == x) return cur;
		cur = cur->next;
	}
	return NULL;
}

4.9 双向链表在pos的前面进行插入

1. 先获取pos前面的节点,将新节点与pos前面的节点连接,然后新节点再与pos连接。

void ListInsert(ListNode* pos, ListDataType x)
{
	assert(pos);

	ListNode* newnode = ListCreate();
	newnode->data = x;

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

4.10 双向链表删除pos位置的节点

1. 获取pos的前后节点,将他们连接,释放pos节点。

void ListErase(ListNode* pos)
{
	assert(pos);

	ListNode* prev = pos->prev;
	ListNode* next = pos->next;
	prev->next = next;
	next->prev = prev;
	free(pos);
}

完整代码:List/List · 林宇恒/DataStructure - 码云 - 开源中国 (gitee.com)

5. 链表编程练习题

5.1 移除链表元素

链接:. - 力扣(LeetCode)

思路:

1. 遍历链表,删除相同值得节点。

2. 使用前后指针,方便节点的释放。

3. 注意特殊情况,当第一个节点就需要删除的时候。

struct ListNode* removeElements(struct ListNode* head, int val) 
{
    struct ListNode* cur = head;
    struct ListNode* prev = NULL;
    while(cur)
    {
        if(cur->val == val)
        {   
            if(prev == NULL) //这里判断的是当前是不是第一个节点,
            {                   //注意,删除完第一个节点后,第二个节点会变成新的第一个节点。
                cur = cur->next;
                free(head);
                head = cur;
            }
            else
            {
                prev->next = cur->next;
                free(cur);
                cur = prev->next;
            }
        }
        else
        {
            prev = cur;
            cur = cur->next;
        }
    }

    return head;
}

5.2 链表的中间结点

链接:. - 力扣(LeetCode)

思路:

1. 快慢指针,slow一次走一步,fast一次走两步。

2. 有奇数节点,偶数节点两种情况,奇数节点时fast走到最后一个节点停下,偶数节点时fast走到空停下。

struct ListNode* middleNode(struct ListNode* head) {
    struct ListNode* slow = head;
    struct ListNode* fast = head;
    while(fast && fast->next)
    {
        slow = slow->next;
        fast = fast->next->next;
    }

    return slow;
}

5.3 合并两个有序链表

链接:. - 力扣(LeetCode)

思路:

1. 将小于或等于的节点尾插到一个新的指针上,返回这个指针。

2. 注意第一个节点尾插需要特殊处理。

struct ListNode* mergeTwoLists(struct ListNode* list1, struct ListNode* list2) 
{
    if(list1 == NULL) return list2;
    if(list2 == NULL) return list1;

    struct ListNode* list3 = NULL;
    struct ListNode* cur = NULL;
    while(list1 && list2)
    {
        if(list1->val <= list2->val)
        {
            if(list3 == NULL) list3 = cur = list1;
            else
            {
                cur->next = list1;
                cur = cur->next;
            }
            list1 = list1->next;
        }
        else
        {
            if(list3 == NULL) list3 = cur = list2;
            else
            {
                cur->next = list2;
                cur = cur->next;
            }

            list2 = list2->next;
        }
    }

    if(list1) cur->next = list1;
    if(list2) cur->next = list2;

    return list3;
}

5.4 反转链表 

链接:. - 力扣(LeetCode)

思路1:用三个指针来实现反转。

struct ListNode* reverseList(struct ListNode* head) 
{
    if(head == NULL) return NULL;

    struct ListNode* n1 = NULL;
    struct ListNode* n2 = head;
    struct ListNode* n3 = head->next;

    while(n2)
    {
        n2->next = n1;
        n1 = n2;
        n2 = n3;
        if(n3) n3 = n3->next;
    }

    return n1;
}

思路2:头插法,每次cur节点对rhead进行头插。

struct ListNode* reverseList(struct ListNode* head) 
{
    struct ListNode* rhead = NULL;
    struct ListNode* cur = head;
    
    while(cur)
    {
        struct ListNode* next = cur->next;

        cur->next = rhead;
        rhead = cur;

        cur = next;
    }

    return rhead;
}

5.5 链表分割

链接:链表分割_牛客题霸_牛客网

思路:

1. 分两个链表,将小于x的尾插一个链表,大于等于x的尾插另一个链表,最后连接起来。

2. 建议用带哨兵位的链表。

3. 连接起来后第二个链表最后记得指向NULL。

ListNode* partition(ListNode* pHead, int x) 
    {
        ListNode* h1 = (ListNode*)malloc(sizeof(ListNode));
        ListNode* h2 = (ListNode*)malloc(sizeof(ListNode));
        ListNode* h1tail = h1;
        ListNode* h2tail = h2;

        ListNode* cur = pHead;
        while(cur)
        {
            if(cur->val < x)
            {
                h1tail->next = cur;
                h1tail = h1tail->next;
            }
            else 
            { 
                h2tail->next = cur;
                h2tail = h2tail->next;
            }

            cur = cur->next;
        }

        h1tail->next = h2->next;
        h2tail->next = NULL;
        pHead = h1->next;
        free(h1);
        free(h2);

        return pHead;
    }

5.6 相交链表

链接:. - 力扣(LeetCode)

思路:

1. 先求两个链表的长度。

2. 长的链表头指针先走,走到和短的链表一样长。

3. 两个指针一起走,直到遇到一样的节点。

struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB) 
{
    int lenA = 0;
    int lenB = 0;
    struct ListNode* cur = headA;
    while(cur)
    {
        lenA++;
        cur = cur->next;
    }
    cur = headB;
    while(cur)
    {
        lenB++;
        cur = cur->next;
    }

    int gap = abs(lenA-lenB);
    struct ListNode* longlist = headA;
    struct ListNode* shortlist = headB;
    if(lenA < lenB)
    {
        longlist = headB;        
        shortlist = headA;  
    }
    while(gap--) longlist = longlist->next;  

    while(longlist != shortlist)
    {
        longlist = longlist->next;
        shortlist = shortlist->next;
    }
        
    return longlist;
}

5.7 环形链表 

链接:. - 力扣(LeetCode)

思路:

1. 利用快慢指针,如果有环那么会相遇,如果没环就走到链表结束。

bool hasCycle(struct ListNode *head) 
{
    struct ListNode *fast = head;
    struct ListNode *slow = head;
    while(fast && fast->next)
    {
        slow = slow->next;
        fast = fast->next->next;
        if(slow == fast) return true;
    }    

    return false;
}

面试问题:

1. 快指针走两步,慢指针走一步,快指针和慢指针一定会相遇吗?

答:一定会,当他们进入环后,距离不断减1直到0。

快指针走n步,慢指针走一步,假设快指针追到慢指针的距离为N,那么N必须是n-1的倍数才能追上。错过之后,N也会发生变化重新计算。

5.8 环形链表返回环节点

链接:. - 力扣(LeetCode)

思路:

1. 结论:设头节点到入环点的距离为L,入环点到相遇点的距离为X,环的长度为C,

那么有:L = n*C - X,其中n为圈数。

一个指针从头节点开始走,一个指针从相遇点开始走,他们会在入环点相遇。

struct ListNode *detectCycle(struct ListNode *head) 
{
    struct ListNode *fast = head;
    struct ListNode *slow = head;
    while(fast && fast->next)
    {
        slow = slow->next;
        fast = fast->next->next;
        if(slow == fast)
        {
            //此时slow是相遇点
            while(head != slow)
            {
                head = head->next;
                slow = slow->next;
            }

            return head;
        } 
    }   

    return NULL;
}

思路2:

1. 将相遇点的下一个节点保存,然后从相遇点开始断开链表,就形成了两条链表。

2. 一条链表是以原本的,另一条是以相遇点的下一个节点为头节点,这两条链表的交点就是入环点。

5.9 随机链表的复制 

链接:. - 力扣(LeetCode)

思路:

1. 将每个节点拷贝一份并在各自节点后面插入。

2. 可得出拷贝的随机指针是源节点的随机指针的下一位。

3. 分开拷贝节点。

struct Node* copyRandomList(struct Node* head) 
{
    if(head == NULL) return NULL;

    struct Node* cur = head;
    while(cur)
    {
        struct Node* tail = cur->next;
        struct Node* new = (struct Node*)malloc(sizeof(struct Node));
        new->val = cur->val;
        cur->next = new;
        new->next = tail;
        cur = tail;
    }

    cur = head;
    while(cur)
    {
        struct Node* tail = cur->next;
        if(cur->random == NULL) tail->random = NULL;
        else tail->random = cur->random->next;
        cur = cur->next->next;
    }

    cur = head;
    struct Node* nwehead = (struct Node*)malloc(sizeof(struct Node));
    struct Node* ret = nwehead;
    while(cur)
    {
        struct Node* copy = cur->next;
        struct Node* tail = copy->next;
        ret->next = copy;
        cur->next = tail;
        cur = tail;
        ret = copy;
    }
    
    return nwehead->next;
}

 

二. 顺序表和链表的区别 

1. 链表可以任意位置插入删除,顺序表适合尾插尾删。

2. 链表按需申请释放空间,顺序表是扩容空间,可能会浪费。

3. 顺序表支持下标随机访问,链表不支持。

  • 47
    点赞
  • 39
    收藏
    觉得还不错? 一键收藏
  • 33
    评论
评论 33
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值