算法面试题-链表反转变形(golang实现)

题目详情:

给定一个单链表的头节点 head,实现一个调整单链表的函数,使得每K个节点之间为一组进行逆序,并且从链表的尾部开始组起,头部剩余节点数量不够一组的不需要逆序。(不能使用队列或者栈作为辅助)

例如: 链表:1->2->3->4->5->6->7->8->null, K = 3。那么 6->7->8,3->4->5,1->2各位一组。调整后:1->2->5->4->3->8->7->6->null。其中 1,2不调整,因为不够一组。

思路分析:

1. 链表反转之前也做过类似的题目。该题目在此基础上增加了难度,且不能使用队列或者栈作为辅助的数据结构,于是我们可以考虑使用递归法来思考这道题目:这里有篇比较好的文章详细介绍了递归法

2. 既然决定使用递归法,那就按照文章描述的三要素去思考一下这个问题了:

2.1 这个函数需要做什么?答案很简单,这个函数就是为我们将一个单链表按照一定的规则排好序。那么不考虑其他条件的前提下可以大概写出最初的函数原型:

type node struct {
	val int
	next *node
}

// 传入原始的单链表头; 输出处理好的链表头
func handle(head *node) (*node) {
    return nil
}

2.2 函数结束的条件是什么?根据题目可知,分组是从尾节点开始的,那么最初的起点也只能从尾节点开始。然后我们可以大致修改一下代码:

type node struct {
	val int
	next *node
}

// 传入原始的单链表头; 输出处理好的链表头
func handle(head *node) (*node) {
    if head.next != nil {
        // 这里返回的内容需要做进一步的思考
        return nil
    }
    return nil
}

2.3 接下来我们需要分析递归函数的等价关系式了,但是这步也是最难的一步,因为我们只要找出关系式,这个题目就算借出来了,所以先从最简单的条件开始分析:

不考虑通用性的情况下,如果此时链表只有3个元素,那么该题目其实就退化成最原始的反转单链表的题目了,我们可以很容易得出以下代码:

func reverse(head *node) *node {
	result, tail := handle(head)
	tail.next = nil
	return result
}

func handle(head *node) (*node, *node) {
	if head.next == nil {
		return head, head
	}
	newHead, temp := handle1(head.next)
	temp.next = head
	return newHead, head
}

从以上代码来看,我们的递归函数,需要2个返回值。一个为最终结果的头节点,一个为尾节点用于在递归时使用。

接下来我们稍微增加点难度,假如我们的链表存在6个元素。那么我们上面的代码就不适用了,需要进行更进一步的修改才行。6个元素的情况下,根据题目需求我们需要将其分为两组,然后在组内将他们进行逆序。然后将2个组的单链表连接起来。于是我们可以将这个过程分解成三个任务

①分组:假设题目为顺序分组,那么我们正常能想到的分组方法是:从头节点编号1,满3个为一组,然后重新编号。按照这个思维,如果逆序分组,那么我们就需要从尾部开始标号,尾节点编号为1,以此类推。所以这里我们又引入了一个过程变量:编号。在逆序编号的情况下,前面的节点是不可能知道后面的节点的编号的,所以我们必须将这个编号让上层感知,即将该变量放到返回值中,返回给上一层。代码如下:

func reverse(head *node) *node {
	result, tail, _ := handle(head)
	tail.next = nil
	return result
}

func handle(head *node) (*node, *node, int) {
	if head.next == nil {
		fmt.Printf("%d - idx: %d\n", head.val, 1)
		return head, head, 1
	}
	newHead, temp, idx := handle1(head.next)
	temp.next = head
	idx = idx + 1
	if idx > 3 {
        // 如果大于3,则说明分组已经满了,则从新分组
		fmt.Printf("%d - idx: %d\n", head.val, 1)
		return newHead, head, 1
	}
	fmt.Printf("%d - idx: %d\n", head.val, idx)
	return newHead, head, idx
}
输出:
8 - idx: 1
7 - idx: 2
6 - idx: 3
5 - idx: 1
4 - idx: 2
3 - idx: 3
2 - idx: 1
1 - idx: 2

那么现在我们分组是已经分好了

②逆序:组内逆序是这道题目的核心难点,硬想比较困难,但是可以结合上一步分组的实现下手。在分组的实现中存在2种情况。第一种为编号不为3,此时应该需要做组内逆序操作。第二种为编号为3,此时代表该组已经分好组了(逆序已经完成)。由于每组之间的逆序过程都是互相不影响的,所以我们需要考虑将已经分好的组保存下来,怎么保存呢?这就要再引入一个递归返回值,该值专门用于保存已经分好组的链表。有了这个变量,我们就可以进行分组逆序了,组内逆序的逻辑其实和正常单链表逆序没什么区别,这里只要使用编号,将逆序范围圈定即可了。于是我们可以得出以下代码:

/*
	返回值说明:
		headNode: 分组中的头部节点
		temNode:  分组的尾部节点
		int:      当前节点在分组中的编号(最大为3)
		done:     已经完成分组的节点
*/
func handle(n *node) (*node, *node, int, *node) {
	if n.next != nil {
		headNode, temNode, idx, done := handle(n.next)
		if idx % 3 == 0{
			// 当idx返回3时,代表上一组已经完成完成分组了。
			if done != nil {
				// 如果当前组不为第一组,那将当前组和done结合起来(当前组一定在前面)
				temNode.next = done
			} else {
				temNode.next = nil
			}
			return n, n, 1, headNode
		} else {
			temNode.next = n
			return headNode, n, idx+1, done
		}
	} else {
		return n, n, 1, nil
	}
}

③连接:有了上一步的思路,其实连接的实现也出来了。但是这里其实功能还没有实现完全,因为题目还说明了,如果存在节点数小于3的组则依旧顺序存放。那么我们就需要考虑这种异常情况,我这个人比较懒。不太想再重新修改之前的逻辑了,所以我打算在这个函数的外面再加一个“优化函数”,专门处理这个特殊情况。我们可以假设输入的单链表长度为3的倍数,那么handle函数的结果正好是最终结果不用做任何的修复。但是如果单链表的长度为3n+1,那么多出来的那个链表必然是原链表的head节点,而根据我们的handle函数实现,最后返回值的temNode肯定为head节点,且idx为1。那么这个时候我们只需要将temNode作为最终结果的head节点即可。如果单链表长度为3n+2的话,这个时候返回值的temNode肯定为原链表的head.next节点,且idx为2。这个时候我们只需要照葫芦画瓢处理一下就可以了。最终代码如下:

package main

import (
	"fmt"
)

type node struct {
	val int
	next *node
}

var head *node

func NewNode(val int, next *node) *node {
	return &node{val, next}
}

func InitData() {
	head = NewNode(1, nil)
	tmp := head
	for i:=2; i<9; i++ {
		t := NewNode(i, nil)
		tmp.next = t
		tmp = t
	}
}

/*
	返回值说明:
		headNode: 分组中的头部节点
		temNode:  分组的尾部节点
		int:      当前节点在分组中的编号(最大为3)
		done:     已经完成分组的节点
*/
func handle(n *node) (*node, *node, int, *node) {
	if n.next != nil {
		headNode, temNode, idx, done := handle(n.next)
		if idx % 3 == 0{
			// 当idx返回3时,代表上一组已经完成完成分组了。
			if done != nil {
				// 如果当前组不为第一组,那将当前组和done结合起来(当前组一定在前面)
				temNode.next = done
			} else {
				temNode.next = nil
			}
			return n, n, 1, headNode
		} else {
			temNode.next = n
			return headNode, n, idx+1, done
		}
	} else {
		return n, n, 1, nil
	}
}

func printList(head *node) {
	for {
		fmt.Print(head.val)
		if head.next == nil {
			break
		} else {
			fmt.Print(" --> ")
		}
		head = head.next
	}
}

func main() {
	InitData()
	nHead, tNode, idx, done := handle(head)
	switch idx {
	case 1:
		// 还有一个剩余
		nHead.next = done
		printList(nHead)
		break
	case 2:
		// 还有两个剩余
		nHead.next = done
		tNode.next = nHead
		printList(tNode)
		break
	case 3:
		// 刚好能分完组
		printList(done)
		break
	}
	printList(nHead)
}

总结:

这道题目上手可能觉得比较复杂,如果在面试情况下自己心态比较紧张的话更加难有思路。我看了网上的解法还有许多,我觉得我这种解法算是硬想出来的,因为最后的关系式我也没有比较标准的写出来。但是我觉得顺着自己的思路走并且能得到解决方法会对自己的提升更大,所以我也将自己的思路给大伙分享下。我的解法不具有普适性,如果面试官要把分组改为可变化的话,我这个代码则不适用了。站在面试官的角度,肯定是想要考验面试者写代码的各个方面,实现功能只是一个基础,如果能在实现功能的基础上能增加代码的可拓展性和适用性,那么将会是你的加分项~

你可能感兴趣的:(面试)