LeetCode 25_Reverse Nodes in k-Group

今天周末了,有点儿闲暇时间,来写一道吧。

刚开始看这题感觉和前面24题一样,所以就有点儿“轻敌”了,编了一会儿才发现不是那么回事儿,问题还是比较难的,后来才发现难度是hard,看来这个难度还是挺准的。其实这个东西难点主要还是链表的指针操作,因为涉及的过程比较复杂,而指针本身就是一个复杂不好操作的东西,所以实现起来就有难度了。还是先看题目吧。

Given a linked list, reverse the nodes of a linked list k at a time and return its modified list.

If the number of nodes is not a multiple of k then left-out nodes in the end should remain as it is.

You may not alter the values in the nodes, only nodes itself may be changed.

Only constant memory is allowed.

For example,
Given this linked list: 1->2->3->4->5

For k = 2, you should return: 2->1->4->3->5

For k = 3, you should return: 3->2->1->4->5

题目的意思就是给你一个链表头结点,和一个数k,然后将链表中每k各个结点翻转一下,同样要求不能改变结点值,也就是必须用指针指来指去的。

这个算法实现方法比较多,最简单的就是循环的方法了,如果你对递归比较熟那可以使实现简单一点儿。我是用的循环方法,那就先来说一下我的思路,然后再找一个大神的递归算法分析一下。

循环算法的思路很直接,就是找到第一个结点,然后找到第k个节点,把他们翻转一下。由于这个操作相对独立,所以我们可以考虑把它用一个单独的函数实现。

之所以说相对独立时因为其实它并不像理想中那么独立,这也正是这个题目的难度所在,前面那道题中我们已经分析过了,第一个结点的指向时不明的,也就是从当前函数的形参中无法获得,它也许指向第k个结点后面的(当k后面结点数不足k时),也许指向第2k个元素。

也就是说它的指向要靠下一次函数调用时的信息中获得。这就给人一种交叉迭代往前推进的感觉,我们要做的就是把这个交叉分离开构成循环。而在分离点就要做一些处理来完成连接。

前面已经提到,这个题目完成过程比较复杂,里面涉及大量的指针操作。所以对指针判空啊,循环界限值啊,该先给哪个指针先赋值啊,会不会造成下一个节点丢失啊等等这些问题比较多比较细,所以大家要细心再细心。

下面看一下代码

<span style="font-size:14px;"><span style="font-size:24px;"> // Definition for singly-linked list.
  struct ListNode {
      int val;
      ListNode *next;
      ListNode(int x) : val(x), next(NULL) {}
  };
 
void reverseKNode(ListNode* pBegin,ListNode* pEnd)
{
	//将指针pBegin到pEnd之间的链表翻转
	if(pBegin==NULL || pEnd==NULL || pBegin==pEnd)
		return;

	ListNode* pFirst = pBegin;
	ListNode* pSecond = pBegin->next;
	ListNode* pThrid = pSecond->next;//可能为空,后面要小心
	pFirst->next = pEnd->next;
	while(pFirst!=pEnd)
	{
		pSecond->next = pFirst;
		pFirst = pSecond;
		pSecond = pThrid;
		if(pThrid != NULL && pThrid != pEnd)
			pThrid = pThrid->next;
	}
}
ListNode* getKNodes(ListNode* pBegin,int k)
{
	//判断从pBegin算起(含pBegin)后面是否还有k个节点
	//若存在返回第k个节点,否则返回NULL
	ListNode* pNode = pBegin;
		for(int i=0;i<k-1;i++)
		{
			if(pNode==NULL) return NULL;
			else
			{
				pNode=pNode->next;
			}
		}
		if(pNode != NULL)
			return pNode;
		else return NULL;
}
class Solution {
public:
    ListNode* reverseKGroup(ListNode* head, int k) {//其实这种传址方式是不对的!
		//判断链表中是否有k个节点,若没有则直接返回
		ListNode* pKth = getKNodes(head,k);
		if(!pKth) return head;
		ListNode* pAfterKth = pKth->next;
		ListNode* pBegin = head;
		//设置头结点
		head = pKth;
		do
		{
			reverseKNode(pBegin,pKth);
			ListNode* pBegin2 = pBegin;
			pBegin = pAfterKth;
			pKth = getKNodes(pAfterKth,k);
			if(pKth)//p和p!=NULL等价 !p和p==NULL等价!
			{
				pBegin2->next = pKth;
				pAfterKth = pKth->next;
			}
			else break;
		}while(1);

		return head;        
    }
};</span></span>

这里主要说几点细节吧:

(1)小心可能为空的指针。前面已经说了,我们把翻转k个节点的功能独立出来作为一个子程序。子程序中指针的操作就不细说了,主要是以pThrid为例来说一下怎么对待可能为空的指针。通过程序入口处的判断我们知道pFirst和pSecond一定不是空的,而pThrid有可能是空的,但让其指向pSecond->next似乎也是合理的,因为pSecond->next为空就为空白,我们可用通过后面的判断来处理这种情况,但这里一定要留心一下,这种依赖于后面的行为是有危险性的。因为指针历遍的尾巴(即用谁来和后面的哨兵比较,确定终止条件)和要处理元素的最后情况可能不完全对应,也就是当尾巴已经达到最后一个而最后面一组数据还未处理,但如果尾巴再往后移动有可能出现非法指针的情况。这个地方是有些头疼的,必须用一些非常规手段来解决(更像是凑出来的答案,没有什么理论,所以每次都有小心翼翼的捋一遍,当然熟能生巧,慢慢会改进的)。所以这种“凑”出来的代码中就会有纰漏,像这里的pThrid就是一个,它可能为空,但后面的代码又没有以它为尾巴,这导致在后面取pThrid->next时出现bug。所以在取其next时一定要先判空(第一次我写时就没判空,当然就犯了大错,运行时指针越界)。这里的道理其实很简单,我们每天都在说,就是指针使用(在这里就是取val或next)之前一定要考虑它是不是空的,只是在编程过程中我们要一边凑代码,一边考虑节点的具体情况,就会忽略这一点。在这里着重提一下,加深印象。

(2)连接过程。前面也已经提到,我们要把一个交叉的过程分离开,而交接过程在主函数中进行。即判断下次后面还有没有2k个节点,如果有则改变前面的第一个指针的指向使其指向第2k个节点。在此之前,指针是指向第k个节点后面的那个节点的。因为如果后面没有2k个节点了,那它本身就应该指向k后面那个节点.在交接过程中还要注意保护现场,我第一次实现时没有加pBegin2,直接用了pBegin。导致了值的提前覆盖,这些细节也要小心。(其实是编码时间太长了,脑子陷入了迷糊状态,所以劝大家要适度休息,不要一直编码)

(3)注意参数传递。要想实参值,我们有两个方式,一是传递指向这个参数的指针,二是用return来直接返回新值(注意接收)。先说第一种:

如果我们不通过return来返回头指针,那传递的头指针应该是ListNode**类型的,对于没有头结点的链表,要在程序中改变头指针,是要传指针的指针的,用第二个指针来作为头结点,通过改变*head来改变头指针的指向。其实这里面暗含一个指针传递深度的问题,我们都知道,传值是不能改变实参的,必须传递地址。而同样的道理传递*head是无法改变实参head值的,一定要传**head,而*head其实就是相当于头结点的next域,通过改变它来改变头结点的指向(其实头指针head没有改变,也无法改变)。这一点是可以推广的,要想改变指针值,必须传指针的指针即二级指针,而一级指针用来作为二级指针的指向对象(只有指向对象才可以改变)来承担改变指针的任务。说白了,要想改变a的值,必须传递指向a的指针。而*head是说明head是一个指针,而并没有指针指向这个head所以是无法修改head值的。再说第二种:这个用return的就比较简单了,但注意一定要用参数接收新的参数(不接收不会显示错误),一定不要以为用*head传了指针就不用接收了,原因前面已经说了。所以为了保持一致性,在对list操作时,我们最好统一用return来返回头指针。

对于我的方法就说这么多吧,来看一下大神的递归代码(在这里感谢rantos22 的分享)

<span style="font-size:14px;">class Solution 
{
public:

    ListNode* reverse(ListNode* first, ListNode* last)
    {
        ListNode* prev = last;

        while ( first != last )
        {
            auto tmp = first->next;
            first->next = prev;
            prev = first;
            first = tmp;
        }

        return prev;
    }

    ListNode* reverseKGroup(ListNode* head, int k) 
    {
        auto node=head;
        for (int i=0; i < k; ++i)
        {
            if ( ! node  )
                return head; // nothing to do list too sort
            node = node->next;
        }

        auto new_head = reverse( head, node);
        head->next = reverseKGroup( node, k);
        return new_head;
    }
};</span>
大神的代码就是很简练的,一是因为用了递归嘛,自然比较简单;二是一些细节处理的很好,值得我们学习啊。

这个代码我就不多说了,算法思路是和上面一样的,大家可以自己分析一下。

说得也不少了,今天就到这里吧。
PS:时间确实比较紧张,一些地方可能有错别字,语言不通畅,大家见谅。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
题目描述: 给定一个字符串,请将字符串里的字符按照出现的频率降序排列。 示例 1: 输入: "tree" 输出: "eert" 解释: 'e'出现两次,'r'和't'都只出现一次。因此'e'必须出现在'r'和't'之前。此外,"eetr"也是一个有效的答案。 示例 2: 输入: "cccaaa" 输出: "cccaaa" 解释: 'c'和'a'都出现三次。此外,"aaaccc"也是有效的答案。注意"cacaca"是不正确的,因为相同的字母必须放在一起。 示例 3: 输入: "Aabb" 输出: "bbAa" 解释: 此外,"bbaA"也是一个有效的答案,但"Aabb"是不正确的。注意'A'和'a'被认为是两种不同的字符。 Java代码如下: ``` import java.util.*; public class Solution { public String frequencySort(String s) { if (s == null || s.length() == 0) { return ""; } Map<Character, Integer> map = new HashMap<>(); for (int i = 0; i < s.length(); i++) { char c = s.charAt(i); map.put(c, map.getOrDefault(c, 0) + 1); } List<Map.Entry<Character, Integer>> list = new ArrayList<>(map.entrySet()); Collections.sort(list, (o1, o2) -> o2.getValue() - o1.getValue()); StringBuilder sb = new StringBuilder(); for (Map.Entry<Character, Integer> entry : list) { char c = entry.getKey(); int count = entry.getValue(); for (int i = 0; i < count; i++) { sb.append(c); } } return sb.toString(); } } ``` 解题思路: 首先遍历字符串,使用HashMap记录每个字符出现的次数。然后将HashMap转换为List,并按照出现次数从大到小进行排序。最后遍历排序后的List,将每个字符按照出现次数依次添加到StringBuilder中,并返回StringBuilder的字符串形式。 时间复杂度:O(nlogn),其中n为字符串s的长度。遍历字符串的时间复杂度为O(n),HashMap和List的操作时间复杂度均为O(n),排序时间复杂度为O(nlogn),StringBuilder操作时间复杂度为O(n)。因此总时间复杂度为O(nlogn)。 空间复杂度:O(n),其中n为字符串s的长度。HashMap和List的空间复杂度均为O(n),StringBuilder的空间复杂度也为O(n)。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值