Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// 题目链接:https://leetcode.cn/problems/shan-chu-lian-biao-de-jie-dian-lcof/?envType=study-plan&id=lcof
// day11/31
// 第 11 天主题为:双指针(简单)
// 包含两道题目:
// 剑指offer18.删除链表的节点
// 剑指offer22.链表中倒数第k个节点

package main

//思路很简单的题目,就是要注意代码的鲁棒性
//链表相关的题目非常考察代码的鲁棒性,谨防访问空节点.Next 这种情况的发生。

//要删除某个节点,我们需要记录要删除节点的前一个节点,将前一个节点的 Next域 指向要删除节点的下一个节点,
//所以我们要保存要删除节点的前一个节点,才能做到删除节点。
//cur=head,一次遍历链表,遍历条件为 cur.Next != nil,如果 cur.Next 是我们要删除的节点,
//cur.Next = cur.Next.Next,然后结束遍历,否则 cur=cur.Next,向后移动节点。最后返回 head。

//有一种情况需要额外判断,就是 head 节点就是要删除的节点,因为我们无法保存该节点的前一个节点,
//针对这种情况,直接返回 head.Next 即可
//在遍历之前,我们就要对这种情况进行判断。

//Definition for singly-linked list.
type ListNode struct {
Val int
Next *ListNode
}

func deleteNode(head *ListNode, val int) *ListNode {
if head.Val == val{
return head.Next
}
cur := head
for cur.Next != nil{
if cur.Next.Val == val{
cur.Next = cur.Next.Next
// 跳出是为了应对要删除的节点为链表最后一个,会造成访问空指针的情况出现
break
}
cur = cur.Next
}
return head
}

//稍微来扩展一下,对应《剑指offer》
//
//如果题目给的是一个链表的头指针 和 一个要被删除的节点,而非节点的值。那我们应该如何处理呢?
//按照如上时间复杂度O(n)的思路当然是可以的,那是不是一定需要找到被删除节点的前一个节点呢?
//答案是否定的,我们可以很方便地找到要删除的节点的下一个节点。如果我们把下一个节点的内容复制到被删除节点上来覆盖该节点原有的内容,再把下一个节点删除,那是不是就相当于把需要删除的节点删除了?

//(这个思路很新奇,在用户的需求中,删除节点的意思就是删除节点值,而非内存空间,要抓住这样的需求和抽象层析的信息不对称来寻求突破)

//上述思路还有一个问题:如果被删除的节点位于链表的尾部,Next域为空,就无法使用了,在这种情况下,我们只能遍历链表,得到该节点的前一个节点,将该节点删除。
//值得注意的是:上述代码仍然不是一段完美的代码,因为它基于这样一种假设:要删除的节点确定在链表中。我们需要O(n)的时间才能判断链表是否包含某一节点。
//在面试的过程中,我们可以和面试官讨论这个假设,这样面试官就会觉得我们考虑问题非常全面。
//
//考察点:
//- 应聘者对链表的编程能力
//- 创新思维能力,这需要应聘者打破常规思维模式。当我们想要删除某一节点时,不一定要删除节点本身,可以先把下一个节点的内容复制出来覆盖要被删除节点的内容,再将下一个节点删除,这种思路不容易想到
//- 代码的鲁棒性,也即应聘者思维的全面性,全面考虑到该节点是链表尾结点或头结点的情况。
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
//题目链接:https://leetcode.cn/problems/lian-biao-zhong-dao-shu-di-kge-jie-dian-lcof/

package main

//这道题比较通俗的解法是两次遍历
//先一次遍历链表,得到链表的长度 n,倒数第 k 个节点,即正数第 n-k+1 个节点,第二次遍历输出该节点即可。
func getKthFromEnd(head *ListNode, k int) *ListNode {
n := 0
cur := head
// 第一次遍历得到链表长度n
for cur != nil{
n ++
cur = cur.Next
}
cur = head
x := n-k+1
//第二次遍历找到整数第n-k+1个节点并返回
for i:=0;i<x-1;i++{
cur = cur.Next
}
return cur
}

//方法2:快慢指针
//使用快慢指针后一次遍历即可得到倒数第 k 个节点
//快慢指针 fast 和 slow 初始化指向 head 节点,fast 指针先走 k 步,此时 fast 和 slow 之间距离为 k,然后两指针同时向前走,
//当 fast 指向链表尾结点的Next域(即空节点)时,slow 指向倒数第 k 个节点,返回 slow 即可。
func getKthFromEnd_2(head *ListNode, k int) *ListNode {
fast,slow := head,head
for i:=0;i<k;i++{
fast = fast.Next
}
for fast != nil{
fast = fast.Next
slow = slow.Next
}
return slow
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
// 题目链接:https://leetcode.cn/problems/liang-ge-lian-biao-de-di-yi-ge-gong-gong-jie-dian-lcof/
// day12/31
// 第 12 天主题为:双指针(简单)
// 包含两道题目:
// 剑指offer25.合并两个排序的链表
// 剑指offer52.两个链表的第一个公共节点
package main

//这道题比较通俗的解法是使用两个切片保存 listA 和 listB 的所有节点,然后双层循环判断节点是否相等,若相等直接返回。
//这种解法时间复杂度 O(mn),空间复杂度(max(m,n)),m和n分别为两链表长度,和题目要求的复杂度相差甚远...

func getIntersectionNode(headA, headB *ListNode) *ListNode {
recordA := []*ListNode{}
cur := headA
for cur != nil{
recordA = append(recordA,cur)
cur = cur.Next
}
recordB := []*ListNode{}
cur = headB
for cur != nil{
recordB = append(recordB,cur)
cur = cur.Next
}
for i:=0;i<len(recordA);i++{
for j:=0;j<len(recordB);j++{
if recordA[i] == recordB[j]{
return recordA[i]
}
}
}
return nil
}


//解法2:双指针
//这种解法非常巧妙,感觉第一次见这道题的话,很难想到
//
//声明链表指针类型变量 x 和 y ,分别指向 A 和 B,x 和 y 先在各自链表上向后移动,移动到空节点时,
//跳到对方链表,依旧同时移动,若两链表存在公共节点,x 和 y 会指向同一节点。存在两种情况
//- 若该节点非空,为两链表的第一个公共节点,返回该节点即可;
//- 该节点为空,说明两指针均走完了两个链表,未发现公共链表,返回 nil。
//
//下面来解释一下:假设链表 A 和 B 的长度分别为 m 和 n,若存在公共节点,设公共链表的长度为 x,将两个链表合并,长度为 m+n,
//若存在公共节点,即 x>0,那 x 和 y 走 m+n-x 步后到达该节点,若不存在公共节点,x 和 y 始终不相等,知道 x 和 y 共同走向合并链表的尽头,
//也就是空节点,此时,返回 A or B 都是空节点。
//
//针对细节,在做一些描述(也是自己曾经的疑惑):
//1. 对一个链表(长度为n),从头结点走到尾结点需要 n-1 步,走 n 步会到达尾结点的Next域(即空节点),在本题的双指针解法中,真是要走到该空节点位置,然后再走到另一条链表的道路,和另一个指针一起判断是否存在公共节点。
//2. 公共节点,该节点不仅Val域相同,Next域也是相同的,所以以该节点作为头结点的链表长度也是相同的。
//3. 若两链表长度相同呢,感觉都走不到对方的链表上面?确实是这样,因为这种情况下我们也不需要走到对方的链表上,具体分两种情况
// 1. 存在公共节点,两指针 x 和 y 在第一条路上就能找到公共节点
// 2. 不存在公共节点,x 和 y 走到各自链表的尾结点的 Next 域时,已经相同,返回空节点即可

// 此解法时间复杂度O(m+n),空间复杂度O(1),满足题目要求(题目描述是O(n),这里我认为已经满足)
func getIntersectionNode_2(headA, headB *ListNode) *ListNode {
x,y := headA,headB
for x != y{
if x != nil{
x = x.Next
} else {
x = headB
}
if y != nil{
y = y.Next
} else {
y = headA
}
}
return x
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
//题目链接:https://leetcode.cn/problems/he-bing-liang-ge-pai-xu-de-lian-biao-lcof/?envType=study-plan&id=lcof

package main

//Definition for singly-linked list.
type ListNode struct {
Val int
Next *ListNode
}

//很经典的链表题目了,计算机考研算法必做题
//我的思路是,新建一个最终返回链表的头结点 head,Val域为0(随便设),Next域为空,之后的链表合并操作在其 Next域进行,最终返回 head.Next 即可。
//再声明一个变量 cur = head,用于合并链表,具体分为两步:
//1. 循环合并:当 l1 与 l2 链表头结点均非空时,判断两者头结点的Val域,谁更小,cur.Next 指向该节点,
// 然后该链表头节点指向其下一个节点,cur也指向其Next域,如此循环,直至 l1 或 l2 为空;
//2. 合并剩余尾部:此时若 l1 链表非空,则 head 指向的链表所有节点值均小于等于 l1 链表剩余节点值,cur.Next 指向 l1 即可;
// 若是 l2 链表非空,同理。

//最终返回 head.Next 即可。
func mergeTwoLists(l1 *ListNode, l2 *ListNode) *ListNode {
head := &ListNode{0,nil}
cur := head
for l1 != nil && l2 != nil{
if l1.Val <= l2.Val{
cur.Next = l1
cur = cur.Next
l1 = l1.Next
} else {
cur.Next = l2
cur = cur.Next
l2 = l2.Next
}
}
if l1 != nil{
cur.Next = l1
}
if l2 != nil{
cur.Next = l2
}
return head.Next
}

//这道题还可以递归解决:
//当 l1 或 l2 链表为空时,无需合并,直接返回另一个非空链表即可。
//否则,判断 l1 和 l2 链表的头结点谁更小,改变其 Next 域,递归地指向 l1 和 l2.Next 合并链表的结果,最后返回该链表头结点。递归结束条件为某一链表为空。
func mergeTwoLists_2(l1 *ListNode, l2 *ListNode) *ListNode {
if l1 == nil{
return l2
}
if l2 == nil{
return l1
}
if l1.Val <= l2.Val{
l1.Next = mergeTwoLists(l1.Next,l2)
return l1
} else {
l2.Next = mergeTwoLists(l1,l2.Next)
return l2
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// 题目链接:https://leetcode.cn/problems/he-wei-sde-liang-ge-shu-zi-lcof/?envType=study-plan&id=lcof
// day13/31
// 第 13 天主题为:双指针(简单)
// 包含三道题目:
// 剑指offer21.调整数组顺序使奇数位于偶数前面
// 剑指offer57.和为s的两个数字
// 剑指offer58-I.翻转单词顺序

package main

//梦回LeetCode第一题:两数之和,有点像是吧,这道题和两数之和的区别在于:数组有序。
//方法 1 :先来暴力解法,双层遍历
//第一层遍历从头开始,第二层遍历从第一层遍历元素的下一个元素开始,若两数之和等于 target,返回即可。
func twoSum(nums []int, target int) []int {
n := len(nums)
for i:=0;i<n;i++{
for j:=i+1;j<n;j++{
if nums[i]+nums[j] == target{
return []int{nums[i],nums[j]}
}
}
}
return []int{0,0}
}
//嗯...和预想的一样,超时了,时间复杂度O(n^2),空间复杂度 O(1)

//方法 2:哈希表
//接下来用 两数之和 的解法,一次遍历,哈希表记录遍历过的数字
func twoSum_2(nums []int, target int) []int {
record := map[int]struct{}{}
for i:=0;i<len(nums);i++{
if _,ok := record[target-nums[i]];ok{
return []int{nums[i],target-nums[i]}
}
record[nums[i]] = struct{}{}
}
return []int{0,0}
}
//时间复杂度O(n),空间复杂度O(n)


//方法三:双指针
//上面这两种解题思路没有用到数组有序的这个非常重要的条件,一定会有更优解!
//我们是如何使用双指针来进行优化的呢?这是对双层遍历的优化,在双层遍历中,第一层遍历,元素从左至右,第二层遍历,元素从第一个元素的下一个元素向右遍历。
//考虑使用数组有序的这个条件,双指针指向最小和最大的元素,若两数之和大于 target,最大元素移向第二大的元素,若小于 target,最小元素移向第二小的元素,
//这样两边同时向 target 靠拢,一次遍历就可以得到结果。

//使用双指针,left 和 right 指针初始化指向数组的第一个和最后一个元素的下标
//
//- 若两指针指向元素之和 大于 target,说明需要减小两数之和,右指针向左移
//- 若两指针指向元素之和 小于 target,说明需要增大两数之和,左指针右移
//- 若相等,将两指针指向元素组合为切片返回即可。
func twoSum_3(nums []int, target int) []int {
left,right := 0,len(nums)-1
for left < right{
if nums[left]+nums[right] > target{
right --
} else if nums[left]+nums[right] < target{
left ++
} else {
return []int{nums[left],nums[right]}
}
}
return []int{0,0}
}
//时间复杂度O(n),空间复杂度O(1),充分利用题目所给条件,为最优解法!
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
//题目链接:https://leetcode.cn/problems/fan-zhuan-dan-ci-shun-xu-lcof/?envType=study-plan&id=lcof

package main

import (
"strings"
)

//我的想法是使用栈,每遇到一个完整的单词,添加到栈中,最后出栈。
//
//具体过程:一次遍历,用 string.Builder 构建字符串,若当前字符不是空格,则写入将当前字符写入 sb,
//若是,则将 sb 的内容写入栈 并 清空内容,准备下一个单词的填充。
func reverseWords(s string) string {
var sb strings.Builder
n := len(s)
if n == 0{
return ""
}
stack := []string{}
for i:=0;i<n;i++{
if s[i]==32 && i>0 && s[i-1]!=32{
stack = append(stack,sb.String())
sb.Reset()
} else if s[i]!=32{
sb.WriteByte(s[i])
}
}
if s[n-1] != 32{
stack = append(stack,sb.String())
}
sb.Reset()
for i:=len(stack)-1;i>=0;i--{
sb.WriteString(stack[i])
sb.WriteByte(' ')
}
res := sb.String()
// 去掉末尾空格
// 同时注意特殊情况:s全为空格,res长度为0,对其进行去除末尾空格操作会索引越界
if len(res)==0{
return ""
}
res = res[:len(res)-1]
return res
}


//还可以用双指针:
//
//声明变量 res 为空字符串,left 和 right 两指针初始化为 0,左右指针分别用于寻找每个单词的左右边界。
//一次遍历字符串,左指针向右移动寻找第一个非空格字符,为要寻找字符串的左边界,然后开始寻找右边界,先将 left 的值赋给 right,然后 right 向右移动,
//直到到达字符串末尾或者寻找到空格字符,此时 left 和 right 分别指向一个单词的左右边界,将其加入 res,然后将 right 赋给 left,开始寻找下一个单词。
//
//具体实现时,注意每加入一个单词的同时,还要添加一个空格,最后返回 res 前,处理掉最后添加的空格。
//
//注意特殊情况的处理,当输入字符串全为空格时,res 长度为空,此时如果对其进行去除空格的操作,会索引越界。
// 方法2:
func reverseWords_2(s string) string {
n := len(s)
left,right := 0,0
res := ""
for left < n{
if s[left] == 32{
left ++
} else {
right = left
for right<n && s[right]!=32{
right ++
}
res = s[left:right] + " " + res
left = right
}
}
if len(res) == 0{
return ""
} else {
return res[:len(res)-1]
}
}
Loading