一直以来算法题刷的比较少,算法这块算是我的弱项。前几天,国内的一家游戏公司,也能算的上是大厂了(上海F4)之一。上来给了一个笔试题,几个选择题还好,基本都是一些基础知识和C++的基础,不难。算法题难度也还好吧,不知道是好多年没有做这种笔试题了也不知道是紧张,妈的,做的很烂,两个算法题都没有写出来。其中一个不算难,链表的操作,不是链表排序,链表翻转的一个变种吧,当时脑子抽了,硬是没写出来。
面试题
原题是这样的,给一个链表 [1,2,3,4,5]
然后转换成 [5,3,1,2,4]
这种格式
找一下规律,其实就是第一个先拿第一个元素,然后把第二个元素放在新的链表后面,第三个元素放在新的链表前面,然后第四个放在新链表的后面,第五个放在新链表前面,以此类推。
其实就是一个链表头插和尾插的结合嘛
直接上代码
package main
import (
"fmt"
)
type Node struct {
Next *Node
Val int
}
//通过数组生成链表
func Make(nums []int) *Node {
head := &Node{}
move := head
for _, num := range nums {
t := &Node{
Val: num,
}
move.Next = t
move = move.Next
}
return head.Next
}
//打印链表
func Print(list *Node) {
for h := list; h != nil; h = h.Next {
fmt.Printf("%d ", h.Val)
}
fmt.Println()
}
func main() {
nums := make([]int, 5)
for i := 1; i <= 5; i++ {
nums[i-1] = i
}
nodes := Make(nums)
Print(nodes)
nodes = TurnList(nodes)
Print(nodes)
}
//转换链表
func TurnList(list *Node) *Node {
i := 0
h := list
t := list
list = list.Next
for list != nil {
if i%2 == 0 {
t.Next = list
list = list.Next
t = t.Next
t.Next = nil
} else {
x := list.Next
list.Next = h
h = list
list = x
}
i++
}
return h
}
初始链表如图:
变换过程:
就是样根据奇偶分别头插和尾插就能完成
这么简单当时就是没写出来。 淦
只写这一个也抬不过瘾了,再来一个链表排序的 leetcode 148
链表排序
排序的方法有好多,但是我感觉适合用于链表的是 插入排序 和 归并排序 尤其是归并排序,简直就是为链表定做的。
先来一个简单的,插入排序吧
插入排序的思想就是选择一个基准,然后把整个序列中的比自己小的放到基准的前面,用数组的形式还是很好理解和操作的。但是链表不能随机访问,所以不能用数组那样的插入排序了,需要做一下变换,看起来有点像是选择排序和插入排序的结合。
不废话了,直接上代码,插入排序:
func InsertSort(list *Node) *Node {
head := &Node{
Next: list,
}
h := head
for h.Next != nil {
t := h
for t.Next != nil {
if t.Next.Val < h.Next.Val {
x := t.Next
t.Next = t.Next.Next
x.Next = h.Next
h.Next = x
} else {
t = t.Next
}
}
h = h.Next
}
return head.Next
}
无序的链表
排序过程
图中有个迷惑的地方,就是蓝色的线是 next
指针的指向,就是一开始的时候,head
h
t
的next指针指向 3
也就是 h
t
自身是指向 head
的,head是一开始的时候,新建的一个临时结点,因为单向链表无法向前查找,只能向后查找,所以要用 next
指针。
同样,在使用 t
和 h
指针的时候,也都是使用 next
用来判断。
比较逻辑 t.Next.Val < h.Next.Val
当 t
的 next
是 3,h
的 next
是 2 时,满足条件,所以交换 2 和 3
如此往复,当 t
走完一趟后,链表中的第一个结点就是最小的了。
当 t
走完一趟后,h指针要后移一个。以此类推,直到 h
和 t
都走完后,就完成了排序。
这个排序方法用的是插入排序的思想加上一些选择排序的方法完成的,时间复杂度还是O(n²)。是稳定的排序。
归并排序
相比插入排序和选择排序,归并排序就要复杂一些了
归并排序的思想是分治法,把整个序列拆分,当拆分到不能再拆分时,子序列就是有序的了。也就是当序列长度是1的时候,序列就是有序的。然后在把子序列有序的合并起来。
归并排序简直就是为了链表排序而生的。如果用数组排序,拆分和重组还是比较复杂的,链表就简单多了。说实话,数组的归并排序我都不会写。
线上代码,归并排序
func MergerSort(list *Node) *Node {
if list == nil || list.Next == nil {
return list
}
fast := list.Next
slow := list
for fast != nil && fast.Next != nil {
fast = fast.Next.Next
slow = slow.Next
}
h1 := MergerSort(slow.Next)
slow.Next = nil
h2 := MergerSort(list)
return Merge(h1, h2)
}
func Merge(list1, list2 *Node) *Node {
head := &Node{}
h := head
for list1 != nil && list2 != nil {
if list1.Val < list2.Val {
h.Next = list1
list1 = list1.Next
} else {
h.Next = list2
list2 = list2.Next
}
h = h.Next
}
if list1 != nil {
h.Next = list1
} else {
h.Next = list2
}
return head.Next
}
归并排序有两个部分,一个是拆分,另一个是合并。
-
拆分
拆分是二分法,也就是每次从中间分开。链表中找到中间位置,最方便的就是快慢指针了,就是一个指针每次移动一个位置,指针每次移动两个位置。当快的指针移动到最后的时候,慢的指针正好移动到链表的中间位置。递归拆分,直到链表长度是1的时候,就完成了拆分。 -
合并
合并的过程其实就是合并两个有序链表。leetcode 21 很简单没什么需要解释的。
简单的把排序过程花了出来。
整个归并排序,不好理解的地方就是递归拆分的过程,其实也不难理解,就是能拆的情况下就一直拆,直到不能拆了为止。
fast := list.Next
slow := list
for fast != nil && fast.Next != nil {
fast = fast.Next.Next
slow = slow.Next
}
当一次拆分完成时,slow
指针指向的就是链表的中间位置,所以 slow
的 next
就是链表的后半部分了,list
指针就是链表的前半部分。
拆分的过程中有个地方需要注意,就是断链,slow.Next = nil
因为在用快慢指针找到链表的中间位置时,并没有把链表切分开,需要手动把链表断开,要不然从头开始遍历,还是能遍历整个链表的。
总结
链表的问题其实没有没有特别难的算法,基本都是一些基础的操作。链表难的地方是因为内存地址不连续,不像数组那样可以随机访问,只能通过指针操作,理解起来没有那么直观所以会觉难。遇到链表问题就画图,基本上图画出来了,问题也基本能解决了。
网友评论