数据结构c语言篇 《二》带头双向循环链表实现以及链表相关面试题(下)

Posted 程序猿是小贺

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构c语言篇 《二》带头双向循环链表实现以及链表相关面试题(下)相关的知识,希望对你有一定的参考价值。

链表

借图镇楼
在这里插入图片描述

双向循环链表的实现

SHList.h

#pragma once

// 带头+双向+循环链表增删查改实现
typedef int LTDataType;
typedef struct ListNode
{
	LTDataType data;
	struct ListNode* next;
	struct ListNode* prev;
}ListNode;

// 创建返回链表的头结点.
ListNode* ListCreate(LTDataType x);
//初始化链表
void DListInit(ListNode** pHead);
// 双向链表销毁
void ListDestory(ListNode** pHead);
// 双向链表打印
void ListPrint(ListNode* pHead);
// 双向链表尾插
void ListPushBack(ListNode* pHead, LTDataType x);
// 双向链表尾删
void ListPopBack(ListNode* pHead);
// 双向链表头插
void ListPushFront(ListNode* pHead, LTDataType x);
// 双向链表头删
void ListPopFront(ListNode* pHead);
// 双向链表查找
ListNode* ListFind(ListNode* pHead, LTDataType x);
// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x);
// 双向链表删除pos位置的节点
void ListErase(ListNode* pos);

SHList.c

#define _CRT_SECURE_NO_WARNINGS
#include"DList.h"
#include<stdio.h>
#include<malloc.h>
#include<assert.h>
#include<stdlib.h>


// 创建返回链表的头结点.
ListNode* ListCreate(LTDataType x)
{
	ListNode* newNode = (ListNode*)malloc(sizeof(ListNode));
	if (NULL == newNode)
	{
		assert(0);
		return NULL;
	}
	newNode->data = x;
	newNode->prev = NULL;
	newNode->next= NULL;
	return newNode;
}

//初始化链表
void DListInit(ListNode** pHead)
{
	assert(pHead);
	*pHead = ListCreate(0);
	(*pHead)->next = *pHead;
	(*pHead)->prev = *pHead;
}


// 双向链表销毁
void ListDestory(ListNode** pHead)
{
	assert(pHead);
	ListNode *cur=(*pHead)->next;
	while (cur != (*pHead))
	{
		(*pHead)->next = cur->next;
		free(cur);
		cur = (*pHead)->next;
	}
	free(*pHead);
	*pHead = NULL;
}

// 双向链表打印
void ListPrint(ListNode* pHead)
{
	assert(pHead);
	ListNode *cur = pHead->next;
	while (cur != pHead)
	{
		printf("%d ", cur->data);
		cur = cur->next;
	}
	printf("\\n");
}

// 双向链表尾插
void ListPushBack(ListNode* pHead, LTDataType x)
{
	ListInsert(pHead, x);
}

// 双向链表尾删
void ListPopBack(ListNode* pHead)
{
	assert(pHead);
	ListErase(pHead->prev);
}

// 双向链表头插
void ListPushFront(ListNode* pHead, LTDataType x)
{
	ListInsert(pHead->next, x);
}

// 双向链表头删
void ListPopFront(ListNode* pHead)
{
	assert(pHead) ;
	ListErase(pHead->next);
}


// 双向链表在pos的前面进行插入
void ListInsert(ListNode* pos, LTDataType x) 
{
	ListNode* newNode = NULL;
	if (pos == NULL)
		return; 
	newNode = ListCreate(x);
	newNode->prev = pos->prev;
	newNode->next = pos;
	pos->prev->next = newNode;
	pos->prev= newNode;
}

// 双向链表删除pos位置的节点
void ListErase(ListNode* pos)
{
	if (pos == NULL)
		return;
	pos->prev->next = pos->next;
	pos->next->prev = pos->prev;
	free(pos);

}


void TestDList()
{
	ListNode* head = NULL;
	DListInit(&head);

	ListPushBack(head, 1);
	ListPushBack(head, 2);
	ListPushBack(head, 3);
	ListPushBack(head, 4);
	ListPushBack(head, 5);
	ListPushBack(head, 6);
	ListPrint(head);

	ListPushFront(head, 0);
	ListPrint(head);

	ListPopFront(head);
	ListPrint(head);

	ListPopBack(head);
	ListPopBack(head);
	ListPopBack(head);
	ListPrint(head);

	ListDestory(&head);
}

test.c
偷懒ing~~~

链表面试题(下)

最近实在是太忙了,很多事情挤在一起,所以我下面题目就不用图解 来展示给大家了,必要的注释在代码里面我都有写到,有问题的朋友可以自己画图理一下思路或者直接私我,有时间我一定会回复的,感谢~

1. 合并两个有序链表

将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
在这里插入图片描述

核心代码段

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     struct ListNode *next;
 * };
 */


struct ListNode* mergeTwoLists(struct ListNode* l1, struct ListNode* l2){
    /*
    合并两个有序链表会有如下三种情况
    1.两个链表都为空,返回NULL
    2.有一个链表为空,返回另一条链表
    第一和第二种可视为一种情况,即一条为空返回另一条链表
    */
    if(l1==NULL)
    return l2;
    if(l2==NULL)
    return l1;
    struct ListNode newHead;
    struct ListNode* cur1=l1;
    struct ListNode* cur2=l2;
    struct ListNode* cur=&newHead;
    /*
    3.都不为空
    遍历两个链表,比较两个节点值大小
    给定一个带头的链表,较小者尾插进新链表,
    直到循环结束
    */
    while(cur1&&cur2)
    {
        if(cur1->val>cur2->val)
        {
            //将cur的next域指向cur2即为将cur2尾插进新链表节点的next
            cur->next=cur2; 
            cur2=cur2->next;
        }
        else
        {
            cur->next=cur1;
            cur1=cur1->next;
        }
            cur=cur->next;
    }
    //循环结束后,必有一条链表为空,即有一条链表尾插完毕
   if(cur1==NULL)
    {
        cur->next=cur2;
    }
    else
    {
        cur->next=cur1;
    }
    return newHead.next;
}

2.链表分割

题目描述:
现有一链表的头指针 ListNode* pHead,给一定值x,编写一段代码将所有小于x的结点排在其余结点之前,且不能改变原来的数据顺序,返回重新排列后的链表的头指针。

核心代码段

/*
struct ListNode {
    int val;
    struct ListNode *next;
    ListNode(int x) : val(x), next(NULL) {}
};*/
class Partition {
public:
    ListNode* partition(ListNode* pHead, int x) {
        if(NULL==pHead)
            return NULL;
        //这里应用带头结点的链表的方式会简单一些
        //则此时创建两个带头结点的空链表
        ListNode smallHead(0);
        ListNode bigHead(0);
        ListNode* cur=pHead;
        ListNode* small=&smallHead;
        ListNode* big=&bigHead;
        while(cur)
        {
            pHead=cur->next;//此时相当于把第一个结点拆下来
            if(cur->val<x)
            {
                small->next=cur;
                small=small->next;
            }
            else{
                big->next=cur;
                big=big->next;
            }
            cur=cur->next;
        }
        //此时可能尾还指向的是原链表的next域,不一定为空
        //最后记得需将big的next域置空,
        big->next=NULL;
        
        //将第一条链表的尾指向第二条链表的首元结点
        small->next=bigHead.next;
        return smallHead.next;
    }
};

3.环形链表

给定一个链表,判断链表中是否有环。

如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。

如果链表中存在环,则返回 true 。 否则,返回 false 。
在这里插入图片描述

核心代码段

 //快慢指针,相遇的话即带环
 /*
 快的走两步,慢的走一步,每走一次距离减一,不会出现套圈情况
 若是快的走三步,四步则有可能会出现套圈情况
 */
bool hasCycle(struct ListNode *head) {
    if(NULL==head)
    return NULL;
    struct ListNode *fast=head;
    struct ListNode *slow=head;
    while(fast&&fast->next)
    {
        fast=fast->next->next;
        slow=slow->next;
        if(fast==slow)
        {
            return true;
        }
    }
    return false;
}

4.环形链表2

给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 -1,则在该链表中没有环。注意,pos 仅仅是用于标识环的情况,并不会作为参数传递到函数中。
说明:不允许修改给定的链表。
在这里插入图片描述在这里插入图片描述
解题思路
1.首先先判断链表里面是否有环
我将判环情况封装进一个函数里
判断程序是从NULL跳出还是从break跳出,则只需要判断循环条件是否为真,若是从break跳出,则此时跳出的结点为相遇点
2.在此简单解释一下: 设从头结点到入口点的长度为L,从入口点到两指针的相遇点长度为X,环的长度为R,fast指针走过的长度为L+X+nR(n=1,2,3…)slow指针走过的长度为L+X;而fast指针走过的路程为slow指针的两倍,则化简一下会得到L=nR-X;
所以会得到以下结论:给定一个指针从链表的开始位置往后走,每次走一步,给定另一个指针从相遇点位置往后走,每次走一步,这两个指针相遇的位置即为入口点(开始入环的第一个结点)

核心代码段

//判断是否有环
struct ListNode *hasCycle(struct ListNode *head) {
    if(NULL==head)
    return NULL;
    struct ListNode *fast=head;
    struct ListNode *slow=head;
    while(fast&&fast->next)
    {
        fast=fast->next->next;
        slow=slow->next;
        if(fast==slow)
        {
            return fast;
        }
    }
    return NULL;
}

struct ListNode *detectCycle(struct ListNode *head) {
    //给一个指针接收上面两个指针的相遇位置
    struct ListNode *pxy=hasCycle(head);
    //给一个指针从头开始走
    struct ListNode *pstart=head;

    if(NULL==hasCycle(head))
    {
        return NULL;
    }
    while(pxy!=pstart)
    {
        pxy=pxy->next;
        pstart=pstart->next;
    }
    return pxy;
}

5.复制带随机指针的链表

给你一个长度为 n 的链表,每个节点包含一个额外增加的随机指针 random ,该指针可以指向链表中的任何节点或空节点。
构造这个链表的 深拷贝。 深拷贝应该正好由 n 个 全新 节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的 next 指针和 random 指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点 。
例如,如果原链表中有 X 和 Y 两个节点,其中 X.random --> Y 。那么在复制链表中对应的两个节点 x 和 y ,同样有 x.random --> y 。
返回复制链表的头节点。
用一个由 n 个节点组成的链表来表示输入/输出中的链表。每个节点用一个 [val, random_index] 表示:
val:一个表示 Node.val 的整数。
random_index:随机指针指向的节点索引(范围从 0 到 n-1);如果不指向任何节点,则为 null 。
你的代码 只 接受原链表的头节点 head 作为传入参数。
在这里插入图片描述

核心代码段

struct Node* copyRandomList(struct Node* head) 
{
    if(head==NULL)
    return NULL;
	struct Node*cur=head;  
    struct Node* newNode=NULL;
    //1.在原链表的每个结点后面都插入一个值相等的结点
    while(cur)
    {
        //申请一个新节点,这个结点必须是malloc出来的
        newNode=(struct Node*)malloc(sizeof(struct Node));
        //对这个结点进行判空
        if(NULL==newNode)
            return NULL;
        newNode->next=newNode->random=NULL;
        newNode->val=cur->val;
        //将新结点插入链表
        newNode->next=cur->next;
        cur->next=newNode;
        cur=newNode->next;
    }
    //2.给新插入的结点的随机指针域赋值
    //注意:新节点的随机指针域指向刚好是前一个结点随机指针域的下一个
    cur=head;//继续从头开始遍历
    while (cur)
    {
        newNode=cur->next;
        if(cur->random)
        {
            newNode->random=cur->random->next;
        }
        cur=newNode->next;
    }
    //3.将新插入的节点从原链表中拆下来
    cur=head;
    struct Node* newHead=cur->next;
    newNode=newHead;//把newNode指针放在cur的next位置
    while(newNode)
    {      
        cur->next=newNode->next;
        cur=newNode;
        newNode=cur->next;
    }
    //cur->next=NULL;
    return newHead;
}

6.对链表进行插入排序

插入排序的动画演示如上。从第一个元素开始,该链表可以被认为已经部分排序(用黑色表示)。
每次迭代时,从输入数据中移除一个元素(用红色表示),并原地将其插入到已排好序的链表中。
在这里插入图片描述
插入排序算法

插入排序是迭代的,每次只移动一个元素,直到所有元素可以形成一个有序的输出列表。
每次迭代中,插入排序只从输入数据中移除一个待排序的元素,找到它在序列中适当的位置,并将其插入。
重复直到所有输入数据插入完为止。

核心代码段


struct ListNode* insertionSortList(struct ListNode* head){
    if(head==NULL)
        return head;
    struct ListNode*cur1=head;//给一个指针遍历此链表
    struct ListNode*newHead=NULL;
    struct ListNode*cur2=NULL;
    struct ListNode*prev=NULL;
    while(cur1)
    {
        head=cur1->next;//依次拆节点
        cur2=newHead;//让cur2指向新链表的头结点
        prev=NULL;//每次遍历都要将prev置空
        while(cur2)//遍历新链表
        {
            if(cur1->val<=cur2->val)
            break;
            prev=cur2;//保留cur2此时的结点
            cur2=cur2->next;//让cur2走到下一个结点
        }
        //此时要插入的val值小于新链表第一个结点值或者新链表没有有效节点
        //则需要将val头插进新链表
        if(prev==NULL)
        {
            cur1->next=newHead;
            newHead=cur1;
        }
        //此时表示val值应插在cur2之前且在prev之后;并且此时prev不为空
        else
        {
            cur1->next=cur2;
            prev->next=cur1;
        }
        cur1=head;
    }
    return newHead;
}

7.删除链表中重复的结点

题目描述:
在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。 例如,链表1->2->3->3->4->4->5 处理后为 1->2->5
在这里插入图片描述
解题思路

由于题目要删除节点,则我们需要至少两个指针
第一个指针cur指向该节点,来进行遍历
第二个指针prev指向Null,用来保存每次遍历之前的该节点
题目未说明重复的结点到底有多少,可能为1,2,3,4…
则我们还需要一个指针end来指向该节点的下一节点
让end指针一直往后走,直到end->val不等于cur->val
要是重复的结点不止两个,我们则需要逐一进行删除
所以还需要一个指针start来保存cur,然后让cur指向它的下一节点
循环删除start,直到end->val等于cur->val
然后让end指针继续往后走,此时会出现两种情况
1.所有结点都已被删除,此时prev==NULL,则需要让pHead=cur
2.后面还有结点,则让prev->next=cur;

核心代码段

class Solution {
public:
    ListNode* deleteDuplication(ListNode* pHead) {
        if(pHead==NULL||pHead->next==NULL)
            return pHead;
        ListNode *cur=pHead;
        ListNode *end=pHead->next;
        ListNode *prev=NULL;
        while(end)//循环初始条件,要end不为空
        {
            //进入循环,先判断cur的val值与end的val值是否相等
            if(cur->val==end->val)
            {
                while(end&&cur->val==end->val)
                {
                    //当cur的val值与end的val值相等时,end指向end的nxrt
                    end=end->next;
                }

                while(end!=cur)
                {
                    //当cur与end不相等时,此时我们找到了val值相等的重复区间
                    //然后对其进行删除
                    ListNode *start=cur;
                    cur=cur->next;
                    free(start以上是关于数据结构c语言篇 《二》带头双向循环链表实现以及链表相关面试题(下)的主要内容,如果未能解决你的问题,请参考以下文章

数据结构:如何用C语言快速实现带头双向循环链表

C语言带头双向循环链表增删改查的实现

C语言带头双向循环链表增删改查的实现

《链表》之带头双向循环链表

双向循环链表增删查改C语言实现

C语言实现双向非循环链表(带头结点尾结点)的节点插入