链表问题处理

今天主要是对一些链表相关问题的理解和处理:

下面所有问题处理的都是这种链表:

struct ListNode 
{
	int val;
	struct ListNode* next;
};

话不多说,我直接进入正题:

1. 删除链表中等于给定值 val 的所有节点。

给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回新的头节点。

思路1:在原链表中进行删除:如果需要头删,那就把head后移并且把首节点释放;用pre来链接cur所释放的节点的下个节点

代码实现

struct ListNode* removeElements(struct ListNode* head, int val) 
{
	struct ListNode* pre = NULL;
	struct ListNode* cur = head;
	while (cur)
	{
		if (cur->val == val)
		{
			//头删
			if (cur == head)
			{
				head = cur->next;//注意头删的情况时头是要后移的
				free(cur);
				cur = head;
			}
			else
			{
				 //删除
	             pre->next = cur->next;//这两步顺序不能错
			     free(cur);//这两步顺序不能错
			     cur = pre->next;
			}
		}
		else
		{
			pre = cur;
			cur = cur->next;
		}
	}
	return head;
}

思路2:创建一个新的链表,遍历原链表,把所有不要删除的节点插入新的链表中
要注意这里返回的是个新的head,需要留意野指针的问题!

代码实现

struct ListNode* removeElements(struct ListNode* head, int val)
{
	struct ListNode* cur = head;
	struct ListNode* tail = NULL;
	head = NULL;
	while (cur)
	{
		if (cur->val != val)
		{
			if (tail == NULL)
			{
				head = tail = cur;//首插
			}
			else
			{
				tail->next = cur;//尾插
				tail = tail->next;
			}
			cur = cur->next;
		}
		else
		{
			struct ListNode* del = cur;
			cur = cur->next;
			free(del);
		}
	}
	if (tail)
	{
		tail->next = NULL;//要留意防止tail的next野指针
	}
	return head;
}

基于这个思路,我们优化一下:来个哨兵位的首节点就不用考虑,tail为NULL的情况了。

代码实现一下:

struct ListNode* removeElements(struct ListNode* head, int val)
{
	struct ListNode* cur = head;
	struct ListNode* tail = NULL;
	//哨兵位的头节点
	head = tail = (struct ListNode*)malloc(sizeof(struct ListNode));
	tail->next = NULL;
	while (cur)
	{
		if (cur->val != val)
		{
			tail->next = cur;
			tail = tail->next;
			cur = cur->next;
		}
		else
		{
			struct ListNode* del = cur;
			cur = cur->next;
			free(del);
		}
	}
    tail->next = NULL;
	return head->next;
}

注意

1.即使有了哨兵位的头节点,我们还是要记得把tail->next置为空以结束链表

2.因为有了哨兵位的头节点的存在,最后return 的应该是哨兵位的头节点所指向的那个节点

2. 反转一个单链表。

给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
逆置问题

思路1:把链表节点从最后一个一个
头插

代码实现:

struct ListNode* reverseList(struct ListNode* head)
{
    struct ListNode* newhead = NULL;
    struct ListNode* cur = head;
    while (cur)
    {
        struct ListNode* next = cur->next;//用来记录原链表中的下个节点 

        //头插
        cur->next = newhead;
        newhead = cur;

        cur = next;
    }
    return newhead;
}

思路2:把指针的方向颠倒 

代码实现

struct ListNode* reverseList(struct ListNode* head)
{
    if(head==NULL)
      return NULL; //即传的是空链表时没有逆置的意义
	struct ListNode* n1, * n2, * n3;
	n1 = NULL;
	n2 = head;
	n3 = n2->next;
	while (n2)
	{
		//逆置
		n2->next = n1;
		//迭代
		n1 = n2;//n1跟着n2走
		n2 = n3;//n2往前走一个
		//注意一下n3为空时,应该规避野指针问题
		if (n3)
		{
            n3 = n3->next;
		}
	}
	return n1;

这里需要注意的是:当传的链表为空链表时,没有逆置它的意义。

 图画的可能有点丑,但是大致的意思是很清晰的表示出来了:以图中得1、2、3、4这四个步骤作为一次颠倒,直到n1走到最后一个节点的时候(也就是n3的next指向NULL)的时候结束。

3. 给定一个带有头结点 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。

给你单链表的头结点 head ,请你找出并返回链表的中间结点。
如果有两个中间结点,则返回第二个中间结点。1 2 3 4 5 6 -> 4

思路
快慢指针我们定义一个慢指针和一个快指针,慢指针一次走一步,快指针每一次从slow指向的位置走两步,当快指针到尽头时,慢指针就在中间

代码实现:

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

这里我们要注意的是:当节点数为奇数和偶数这两种情况是不同的

当节点数为奇数时,slow指向中间节点时fast->NULL

当节点数为偶数时,slow指向中间节点时fast指向最后一个节点

4. 输入一个链表,输出该链表中倒数第k个结点。

输入一个链表,输出该链表中倒数第k个结点。
思路:给快慢指针:快指针先走k步,之后和慢指针一起走,当快指针到NULL时,慢指针就指向倒数第k个节点。

代码实现:

struct ListNode* FindKthToTail(struct ListNode* pListHead, int k) 
{
	struct ListNode* slow=pListHead;
	struct ListNode* fast = pListHead;
	int end = k;
	while (end )
	{
		if (fast)
		{
			fast = fast->next;
		}
		else
			return NULL;
		end--;
	}
	while (fast)
	{
		slow = slow->next;
		fast = fast->next;
	}
	return slow;
}

5. 将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。

思路:分别用两个指针指向两链表的第一个节点,从头比较,取小的尾插到新链表——归并

代码实现:

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

  //上面这是不带哨兵位的写法,下面是带哨兵位的写法
  
    struct ListNode* head, * tail;
    head = tail = malloc(sizeof(struct ListNode));
    
    tail->next = NULL;
    while (list1 && list2)
    {
        if (list1->val < list2->val)
        {
            tail->next = list1;
            tail = tail->next;
            list1 = list1->next;
        }
        else
        {
            tail->next = list2;
            tail = tail->next;

            list2 = list2->next;
        }
    }
    if (list1)
    {
        tail->next = list1;
    }
    if (list2)
    {
        tail->next = list2;
    }
    struct ListNode* list = head->next;//由于哨兵位的存在
    free(head);
    return list;
}

6. 编写代码,以给定值x为基准将链表分割成两部分,所有小于x的结点排在大于或等于x的结点之前

现有一链表的头指针 ListNode* pHead,给一定值x,编写一段代码将所有小于x的结点排在其余结点之前,且不能改变原来的数据顺序,返回重新排列后的链表的头指针。
思路:创建两个哨兵位的头节点,将比x小得尾插到第一个链表,将比x大的尾插到第二个链表。

这里如果不用哨兵位,会在这两个任意一个链表为空时带来麻烦。

代码实现:

struct ListNode* partition(struct ListNode* pHead, int x) 
{
    //哨兵位的首节点的创建
    struct ListNode* greaterhead, * greatertail, * lesshead, * lesstail;
    greaterhead = greatertail = (struct ListNode*)malloc(sizeof(struct ListNode));
    lesshead = lesstail = (struct ListNode*)malloc(sizeof(struct ListNode));
    greatertail->next = NULL;
    lesstail->next = NULL;

    //尾插
    struct ListNode* cur = pHead;
    while (cur)
    {
        if (cur->val < x)
        {
            lesstail->next = cur;
            lesstail = lesstail->next;
        }
        else
        {
            greatertail->next = cur;
            greatertail = greatertail->next;
        }
        cur = cur->next;
    }

    lesstail->next = greaterhead->next;//因为有哨兵位,所以是greaterhead的next
    
    greatertail->next = NULL;//这是为了防止greatertail的next不不指向NULL这个特殊情况
    //例如输入 315867 x=7
    
    struct ListNode* head = lesshead->next;//同上面一个道理因为有哨兵位所以是lesshead的next
   //送走大头兵
    free(greaterhead);
    free(lesshead);
    return head;
}

7. 链表的回文结构。

对于一个链表,请设计一个时间复杂度为O(n),额外空间复杂度为O(1)的算法,判断其是否为回文结构。
给定一个链表的头指针A,请返回一个bool值,代表其是否为回文结构。
测试样例:1->2->2->1 返回:true

思路:先找到
中间节点,然后逆置后半(包括中间节点),如果前半和后半节点一个一个往后比较都是相同的,那就是回文结构

代码实现:

这里偷个懒,调用一下上面我们已经写过的找中间节点和逆置的函数

class PalindromeList 
{
public:
    bool chkPalindrome(ListNode* A) 
    {		
		struct ListNode* head = A;
		struct ListNode* mid = middleNode(A);
		struct ListNode* rhead = reverseList(mid);
		while (head && rhead)
		{
			if (head->val == rhead->val)
			{
				head = head->next;
				rhead = rhead->next;
			}
			else
				return false;
		}

		return true;
    }
};

注意:

当前半或后半任意一个链表走到尽头了,就代表它全部和另一个链表相等,原链表就是回文结构,结束程序。

这里牛客网上面没有支持c的选项,但是c++是兼容c的,我们直接在里面写c不管它,它自己会认

这里我们其实是侵入式编程了,把人家的东西改了,最好要给他还原成原来的样子,但是这里题目没要求,所以我们放肆一回吧。

8. 输入两个链表,找出它们的第一个公共结点。

给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null 。
例如 a1->a2->b->c->d->e->NULL  和  a->b->->c->d->e->NULL  相交,在b这个节点相交
思路:所谓相交,就是
两个链表自某个节点之后所有节点都相同了
方法1:从第一个链表的第一个节点开始,拿它和另外一个链表的所有节点比较,如果都不相同,那就往后走,这个方法的时间复杂度为O(N的平方),效率低下。
方法2:把两个链表的长度先都求出来,让长的先走长度差距的步数,之后同时走看看有没有相同的节点。

代码实现:

struct ListNode* getIntersectionNode(struct ListNode* headA, struct ListNode* headB) 
{
    if (headA == NULL || headB == NULL)
    return NULL;
    struct ListNode* curA = headA, * curB = headB;
    int lenA = 1;
    int lenB = 1;
    while (curA->next)
    {
        curA = curA->next;
        lenA++;
    }
    while (curB->next)
    {
        curB = curB->next;
        lenB++;
    }
    if (curA != curB)//如果到最后都不相等,那么说明中间就没有交点
    {
        return NULL;
    }
    //求第一个交点
    struct ListNode* shortList = headA, longList = headB;
    if (lenA > lenB)
    {
        shortList = headB;
        longList = headA;
    }//这是个很聪明的做法
    int gap = abs(lenA - lenB);

    //长的先走差距步数

    while (gap--)
    {
        longList = longList->next;
    }

    while (shortList != longList)
    {
        //同时走
        longList = longList->next;
        shortList = shortList->next;
    }

    return shortList;
}

注意:上面这串代码里面我们没用if语句来分别处理lenA和lenB谁大的情况,而是直接用判断加一个if修改,这是个很聪明的做法。

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

给你一个链表的头节点 head ,判断链表中是否有环。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 
为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况。
如果链表中存在环 ,则返回 true .否则,返回 false 。

思路:带环链表一旦遍历就会死循环,很烦

我们可以用龟兔赛跑的快慢指针方法来解决:
我们定义两个指针来指向这个链表的首个节点,当快指针进入环的时候慢指针可能还没进入,当快指针和慢指针全部进入环了之后,他们
会在某个时刻相遇的。
如果没环,那fast就可以到NULL了。

代码实现:

bool hasCycle(struct ListNode* head) 
{
    struct ListNode* fast = head, * slow = head;
    while (fast && fast->next)
//快指针是要每次走两步的,所以也要考虑fast->next是不是NULL
//1.防止野指针2.当fast->next->NULL时,也就相当于走到了尽头了。
    {
        slow = slow->next;
        fast = fast->next->next;
        if (slow == fast)
        {
            return true;
        }
    }
    return false;
}

最后留一个思考题:快指针走3步可以吗?4步呢?n步呢?

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值