Fork me on GitHub

23. 构建前缀树(字典树)

正文

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
class TrieNode {
val children = HashMap<Char, TrieNode>()
var isWord = false
}

class Trie {
private val root = TrieNode()

fun insert(word: String) {
var node = root
for (char in word) {
if (!node.children.containsKey(char)) {
node.children[char] = TrieNode()
}
node = node.children[char]!!
}
node.isWord = true
}

fun search(word: String): Boolean {
var node = root
for (char in word) {
if (!node.children.containsKey(char)) {
return false
}
node = node.children[char]!!
}
return node.isWord
}

fun startsWith(prefix: String): Boolean {
var node = root
for (char in prefix) {
if (!node.children.containsKey(char)) {
return false
}
node = node.children[char]!!
}
return true
}
}

1. 两数之和

题目

给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标。

你可以假设每种输入只会对应一个答案。但是,数组中同一个元素在答案里不能重复出现。

你可以按任意顺序返回答案。

示例 1:

1
2
3
输入:nums = [2,7,11,15], target = 9
输出:[0,1]
解释:因为 nums[0] + nums[1] == 9 ,返回 [0, 1] 。

示例 2:

1
2
输入:nums = [3,2,4], target = 6
输出:[1,2]

示例 3:

1
2
输入:nums = [3,3], target = 6
输出:[0,1]

提示:

  • 2 <= nums.length <= 10^4
  • -109 <= nums[i] <= 10^9
  • -109 <= target <= 10^9
  • 只会存在一个有效答案

进阶:你可以想出一个时间复杂度小于 O(n2) 的算法吗?

简介

1
2
3
4
这道题的关键在于对 hashmap 查找时间复杂度 O(1) 的应用
关键词:
HashMap<Key, Value>
hashmap.containsKey(Value)

正文

1
2
3
4
5
6
7
8
9
10
11
12
13
class Solution {
fun twoSum(nums: IntArray, target: Int): IntArray {
val hashmap = HashMap<Int, Int>()
for(i in nums.indices){
val complement = target - nums[i]
if(hashmap.containsKey(complement)){
return intArrayOf(hashmap[complement]!!, i)
}
hashmap[nums[i]] = i
}
return intArrayOf()
}
}

2. 两数相加

题目

给你两个 非空 的链表,表示两个非负的整数。它们每位数字都是按照 逆序 的方式存储的,并且每个节点只能存储 一位 数字。

请你将两个数相加,并以相同形式返回一个表示和的链表。

你可以假设除了数字 0 之外,这两个数都不会以 0 开头。

示例 1:

img

1
2
3
输入:l1 = [2,4,3], l2 = [5,6,4]
输出:[7,0,8]
解释:342 + 465 = 807.

示例 2:

1
2
输入:l1 = [0], l2 = [0]
输出:[0]

示例 3:

1
2
输入:l1 = [9,9,9,9,9,9,9], l2 = [9,9,9,9]
输出:[8,9,9,9,0,0,0,1]

提示:

  • 每个链表中的节点数在范围 [1, 100]
  • 0 <= Node.val <= 9
  • 题目数据保证列表表示的数字不含前导零

简介

1
2
3
4
遍历两个链表,元素相加生成新列表。
关键词:
哨兵节点 val dummy = ListNode(0)
关键返回值 dummy?.next

正文

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Solution {
fun addTwoNumbers(l1: ListNode?, l2: ListNode?): ListNode? {
// 定义两个指针 p1 和 p2,分别指向两个链表的头节点
var p1 = l1
var p2 = l2

// 定义一个哨兵节点 dummy,作为结果链表的头节点
val dummy = ListNode(0)
// 定义一个节点 cur, 作为结果链表的尾节点
var cur = dummy

var carry = 0

while(p1 != null || p2 != null){
val x = p1?.`val` ?: 0
val y = p2?.`val` ?: 0
var sum = x + y + carry
carry = sum / 10
sum = sum % 10

cur?.next = ListNode(sum)

p1 = p1?.next
p2 = p2?.next
cur = cur?.next
}

if(carry > 0){
cur?.next = ListNode(carry)
}

return dummy?.next
}
}

3. 无重复字符的最长子串

题目

给定一个字符串 s ,请你找出其中不含有重复字符的 最长子串 的长度。

示例 1:

1
2
3
输入: s = "abcabcbb"
输出: 3
解释: 因为无重复字符的最长子串是 "abc",所以其长度为 3。

示例 2:

1
2
3
输入: s = "bbbbb"
输出: 1
解释: 因为无重复字符的最长子串是 "b",所以其长度为 1。

示例 3:

1
2
3
4
输入: s = "pwwkew"
输出: 3
解释: 因为无重复字符的最长子串是 "wke",所以其长度为 3。
请注意,你的答案必须是 子串 的长度,"pwke" 是一个子序列,不是子串。

提示:

  • 0 <= s.length <= 5 * 10^4
  • s 由英文字母、数字、符号和空格组成

简介

1
2
3
4
5
巧妙的使用 HashMap<Char, Int> 记录每个字符的最新的位置
巧妙的确定了每个元素对应的滑动窗口的左边界
关键词:
HashMap <Char, Int>用以查找位置
窗口长度不固定,遍历指针作为左边界

正文

本题的巧妙在于使用 HashMap 和 遍历指针构建了一个滑动窗口

在寻找滑动窗口的时候,我们总是固定住一端位置去寻找另一端的位置,通常需要我们找到两个端点之间的关系

来分析滑动窗口的性质:左边届位置,右边界的位置,滑动窗口的长度
三者有以下这些关系:滑动窗口的长度 = 滑动窗口右边届 - 滑动窗口左边界
无论算法如何变化,我们知二求一

我们分析,遍历指针和左右边界的关系有三
情况一:遍历指针是滑动窗口的左边界
情况二:遍历指针是滑动窗口的右边界
情况三:遍历指针在滑动窗口的中间

结合滑动窗口的性质
情况一:知道窗口的长度,以遍历指针为左边界
情况二:窗口长度不固定,新增的元素决定窗口的长度,也就是左边界的位置
情况三:对于滑动窗口算法,通常情况下遍历指针要么位于窗口的左边界,要么位于右边界,用于控制窗口的扩展和收缩。在常规的滑动窗口算法中,遍历指针并不位于窗口的中间位置。

显然这种是情况二

当滑动窗口的位置和长度变化受制于新增的元素时,我们将遍历指针设置为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Solution {
fun lengthOfLongestSubstring(s: String): Int {
var maxLen = 0 // 最长不含重复字符子串的长度
var left = 0 // 窗口左边界
val map = HashMap<Char, Int>() // 哈希表记录字符最后出现的位置

for(right in s.indices){
val char = s[right]

if(map.containsKey(char) && map[char]!! >= left){
left = (map[char]?: 0) + 1
}

map[char] = right
maxLen = maxOf(maxLen, right - left + 1)
}
return maxLen
}
}

4. 寻找两个正序数组的中位数

题目

给定两个大小分别为 mn 的正序(从小到大)数组 nums1nums2。请你找出并返回这两个正序数组的 中位数

算法的时间复杂度应该为 O(log (m+n))

示例 1:

1
2
3
输入:nums1 = [1,3], nums2 = [2]
输出:2.00000
解释:合并数组 = [1,2,3] ,中位数 2

示例 2:

1
2
3
输入:nums1 = [1,2], nums2 = [3,4]
输出:2.50000
解释:合并数组 = [1,2,3,4] ,中位数 (2 + 3) / 2 = 2.5

提示:

  • nums1.length == m
  • nums2.length == n
  • 0 <= m <= 1000
  • 0 <= n <= 1000
  • 1 <= m + n <= 2000
  • -10^6 <= nums1[i], nums2[i] <= 10^6

简介

1
2
3
4
5
6
7
8
9
10
11
12
讨论在两个有序数组中寻找中位数的问题

观察单个有序数组,通过比较元素大小找到舍弃的元素,确定中位数。

类推两个有序数组,准备两个区间来存放偏小值和偏大值。
根据区间需要满足必要条件,使得偏小区间的元素都小于偏大区间。
问题转化为在单个有序数组中查找满足条件的关键元素位置,可以使用二分查找。

关键词:
偏小区间
偏大区间
二分查找

正文

首先思考我们是否可以避免全排序找到中位数

我们有一种想法就是通过比较元素的大小找到需要舍弃的元素

我们观察一个有序数组,我们准备两个区间 RSmall, RBig 分别存放这个数组的偏小值与偏大值(相对中位数而言)

分别用 left,right 指针从两头操作偏小值与偏大值,将他们依次放入对应的数组

直到 「偶数时 RSmall.size = RBig.size 或者 奇数时 RSmall.size = RBig.size + 1」,中位数显然易得

好,现在我们来看两个有序数组的复杂情况

我们同样准备两个区间 RSmall, RBig,同样我们需要将数组偏小值放进 RSmall,偏大值放进 RBig

与一个有序数组不同的是,现在我们必须综合考虑两个有序数组的情况

所以 RSmall 区间里可能既包含 num1 数组的偏小元素也包含 num2 数组的偏小元素,对于RBig也是一样

这就让我们的思考变得复杂了,这使得 RSmall,RBig 变得无序

现在将这个 RSmall 再细分为 RSmallN1 区间,以及 RSmallN2 区间,同样有 RBigN1,RBigN2 区间

想要得到中位数,那么偶数时 RSmall.size = RBig.size 或者 奇数时 RSmall.size = RBig.size + 1

也就有

「 必要条件一: 偶数时 RSmallN1.size + RSmallN2.size = RBigN1.size + RBigN2.size 奇数时 RSmallN1.size + RSmallN2.size = RBigN1.size + RBigN2.size +1 」

同时 RSmall 区间内任何一个元素都必须小于 RBig 区间

自然有 Small 区间的子区间的任何一个元素都小于 Big 区间的子区间的任何一个元素

所以得到

「 必要条件二: RSmallN1 的最大值 < RBigN1 的最小值 ( 因为数组有序,这是不用判断的 ) RSmallN1 的最大值 < RBigN2 的最小值 RSmallN2 的最大值 < RBigN1 的最小值 RSmallN2 的最大值 < RBigN2 的最小值 ( 因为数组有序,这是不用判断的 )」

简化一下

「 必要条件二: maxOf( RSmallN1 ) < minOf ( RBigN2 ) maxOf( RSmallN2 ) < minOf ( RBigN1 ) 」

显然必要条件一加上必要条件二就是充分必要条件

现在我们要得到这四个区间,或者说要在两个有序数组分别划分出这四个区间

我们要在这两个数组中分别找到关键的那个分隔元素

因为必要条件一的原因,我们找到了 num1 数组的分割元素,我们也就找到了 num2 数组的分割元素

这非常好理解,因为 Small 区间和 Big 区间的数量是固定的,RSmallN1 多了一个元素,RSmallN2 就要对应减少一个元素

所以我们只要找到 num1 数组的分隔元素即可

至此,问题成功转化成了

在 num1 数组中查找出一个关键元素位置,这个关键元素满足上述两条必要条件

这是一个典型的有判断条件的查找问题,我们就可以使用二分查找,测试每个元素是否符合上述两个条件

关于二分查找这里不再赘述。

以下是基于二分查找的 Kotlin 代码实现,用于找出两个有序数组的中位数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// 该函数用于在两个已排序整数数组中找到它们的中位数,返回类型为 Double。
fun findMedianSortedArrays(nums1: IntArray, nums2: IntArray): Double {
// 获取 nums1 和 nums2 的长度。
val n1 = nums1.size
val n2 = nums2.size
// 如果 nums1 的长度大于 nums2,则交换它们,确保 nums1 的长度小于等于 nums2。
if (n1 > n2) {
return findMedianSortedArrays(nums2, nums1)
}
// 设置左右指针的初始值。
var left = 0 // 如果把 num1 和 num2 合并元素重新排序,最左边
var right = n1 // 如果把 num1 和 num2 合并元素重新排序,相当于 num2 最左边
// 当左指针小于等于右指针时,执行二分查找。
while (left <= right) { // 因为 n1 < n2
// 计算 nums1 中间的索引 i 和 nums2 中间的索引 j。
val num1HalfIndex = (left + right) / 2
val num2HalfIndex = ((n1 + n2 + 1) / 2) - num1HalfIndex
// 计算 nums1 和 nums2 的左侧和右侧的最大值和最小值。
val maxOfNum1SmallRange = if (num1HalfIndex == 0) Int.MIN_VALUE else nums1[num1HalfIndex - 1] // nums1 小区间的最大值
val minOfNum1BigRange = if (num1HalfIndex == n1) Int.MAX_VALUE else nums1[num1HalfIndex] // nums1 大区间的最小值
val maxOfNum2SmallRange = if (j == 0) Int.MIN_VALUE else nums2[j - 1] // nums2 小区间的最大值
val minOfNum2BigRange = if (j == n2) Int.MAX_VALUE else nums2[j] // nums2 大区间的最小值
// 如果满足条件,则返回中位数。
if (maxOfNum1SmallRange <= minOfNum2BigRange && maxOfNum2SmallRange <= minOfNumBigRange) { // num1小 < num2大 && num2小 < num1大, 也就是说“小区间都小于大区间”
return if ((n1 + n2) % 2 == 0) { // 如果 nums1 和 nums2 的长度之和为偶数
// 则返回左右两侧最大值和最小值的平均值。
(maxOf(maxOfNum1SmallRange, maxOfNum2SmallRange).toDouble() + minOf(minOfNum1BigRange, minOfNum2BigRange).toDouble()) / 2.0
} else {
// 如果 nums1 和 nums2 的长度之和为奇数,则返回左侧最大值。
maxOf(maxOfNum1SmallRange, maxOfNum2SmallRange).toDouble()
}
} else if (maxOfNum1SmallRange > minOfNum2BigRange) { // num1大 < num2小
// 如果 nums1 的左侧最大值大于 nums2 的右侧最小值,则需要将 num1HalfIndex 向左移动。
/*
left num1HalfIndex right
| | |
1 1 1 1 1 1 1 1 1 1 1 1 E
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 E
|
num2HalfIndex
*/
right = num1HalfIndex - 1
/*
num1HalfIndex
left | right
| | |
1 1 1 1 1 1 1 1 1 1 1 1
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 E
|
num2HalfIndex
*/
} else {
// 如果 nums2 的左侧最大值大于 nums1 的右侧最小值,则需要将 num1HalfIndex 向右移动。
/*
left num1HalfIndex right
| | |
1 1 1 1 1 1 1 1 1 1 1 1 E
1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 E
|
num2HalfIndex
*/
left = num1HalfIndex + 1
/*
留给大家画
*/
}
}
// 如果找不到中位数,则返回 0.0。
return 0.0
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
fun findMedianSortedArrays(nums1: IntArray, nums2: IntArray): Double {
val n1 = nums1.size
val n2 = nums2.size
if (n1 > n2) {
return findMedianSortedArrays(nums2, nums1)
}
var left = 0
var right = n1
while (left <= right) { // 因为 n1 < n2
val num1HalfIndex = (left + right) / 2
val num2HalfIndex = ((n1 + n2 + 1) / 2) - num1HalfIndex
val maxOfNum1SmallRange = if (num1HalfIndex == 0) Int.MIN_VALUE else nums1[num1HalfIndex - 1]
val minOfNum1BigRange = if (num1HalfIndex == n1) Int.MAX_VALUE else nums1[num1HalfIndex]
val maxOfNum2SmallRange = if (j == 0) Int.MIN_VALUE else nums2[j - 1]
val minOfNum2BigRange = if (j == n2) Int.MAX_VALUE else nums2[j]
if (maxOfNum1SmallRange <= minOfNum2BigRange && maxOfNum2SmallRange <= minOfNumBigRange) {
return if ((n1 + n2) % 2 == 0) {
(maxOf(maxOfNum1SmallRange, maxOfNum2SmallRange).toDouble() + minOf(minOfNum1BigRange, minOfNum2BigRange).toDouble()) / 2.0
} else {
maxOf(maxOfNum1SmallRange, maxOfNum2SmallRange).toDouble()
}
} else if (maxOfNum1SmallRange > minOfNum2BigRange) {
right = num1HalfIndex - 1
} else {
left = num1HalfIndex + 1
}
}
return 0.0
}

留给大家一个问题,问 N 个有序数组的中位数怎么求?

5. 最长回文子串

题目

给你一个字符串 s,找到 s 中最长的回文子串。

如果字符串的反序与原始字符串相同,则该字符串称为回文字符串。

示例 1:

1
2
3
输入:s = "babad"
输出:"bab"
解释:"aba" 同样是符合题意的答案。

示例 2:

1
2
输入:s = "cbbd"
输出:"bb"

提示:

  • 1 <= s.length <= 1000
  • s 仅由数字和英文字母组成

简介

1
2
3
4
5
介绍 Manacher 算法
理解回文串的对称性,减少与回文串相交的字符串的计算量
关键词
情况一:完全包含,直接赋值
情况二:部分相交。直接从后一位接续计算

正文

数据预处理: 首先回文子串有两种形式 奇数 与 偶数 也就有两种对应的指针操作方式

假定有字符数组 ababaabc 改成 # a # b # a # b # a # a # b # c # 将偶数数组变成奇数统一处理 索性改成 ^ # a # b # a # b # a # a # b # c # $,头尾清晰
这样就可以通过把每个字符作为回文子串的中心向两边扩展,找出最长回文子串 时间复杂度是 O(n^2)

现在需要我们观察回文子串的规律,简化计算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
      0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8
^ # a # b # a # b # a # a # b # c # $
0 ^ P[0] = 0
1 ^ P[1] = 0
2 --^-- p[2] = 1
3 ^ P[3] = 0
4 ------^------ P[4] = 3
5 ^ P[5] = 0
6 ----------^---------- P[6] = 5
7 ^ P[7] = 0
8 ------^------ P[8] = 3
9 ^ P[9] = 0
10 --^-- P[10] = 1
11 --------^-------- P[11] = 2
12 --^-- P[12] = 0
13 ^ P[13] = 0
14 --^-- P[14] = 1
15 ^ P[15] = 0
16 --^-- P[16] = 1
17 ^ P[17] = 0
18 ^ P[18] = 0

情况一:

1
2
3
10                      --^--                P[10] = 1
11 --------^-------- P[11] = 2
12 --^-- P[12] = 0

第 10 行,第 12 行 都是 第 11 行 的子串,完全包含在 第 11 行 之中,由于回文串的对称性 此时直接有 P[10] = P[12]

情况二:

1
2
3
4       ------^------                        P[4]  = 3
6 ----------^---------- P[6] = 5
8 ------^------ P[8] = 3

第 4 行,第 8 行 都是 第 6 行 的子串,分别位于字符串的两端,当我们知道 第 4 行 的信息之后,我们知道 第 8 行 至少有 第 4 行 那么长,至于会不会更长,继续试着向两边扩展即可 此时需要干两件事 1. 将 P[8] = P[4]; 2. 继续向外扩展

情况三: 8 ——^—— P[8] = 3 11 ——–^——– P[11] = 2 14 –^– P[14] = 1 第 8 行 部分与 第 11 行 重叠,第 14 行 是 第 11 行 的子串,非常简单,舍弃超出部分 8 –^– P[8] = 3 11 ——–^——– P[11] = 2 14 –^– P[14] = 1 当成这样处理即可 定义遍历指针 i ,指向回文的中心的指针 center 和 指向回文串右边界的指针 r 此时需要干两件事 1. 将 P[8] = r - i ; 2. 继续向外扩展

好,我们现在已经理解了 Manacher 算法的精髓了 我们思考一下算法该怎么写

P[i] 计算过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
      0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8
^ # a # b # a # b # a # a # b # c # $
0 ^ P[0] = 0 -- 起始,不必计算,更新 r
1 ^ P[1] = 0 -- 暴力计算,一次计算,更新 r
2 --^-- p[2] = 1 -- 暴力计算,二次计算,更新 r
3 ^ P[3] = 0 -- 情况二,一次计算
4 ------^------ P[4] = 3 -- 暴力计算,四次计算,更新 r
5 ^ P[5] = 0 -- 情况一,零次计算
6 ----------^---------- P[6] = 5 -- 情况二,六次计算,更新 r
7 ^ P[7] = 0 -- 情况一,零次计算
8 ------^------ P[8] = 3 -- 情况二,一次计算
9 ^ P[9] = 0 -- 情况一,零次计算
10 --^-- P[10] = 1 -- 情况二,一次计算
11 --------^-------- P[11] = 2 -- 情况二,五次计算,更新 r
12 --^-- P[12] = 0 -- 情况一,零次计算
13 ^ P[13] = 0 -- 情况一,零次计算
14 --^-- P[14] = 1 -- 情况三,一次计算
15 ^ P[15] = 0 -- 情况二,一次计算
16 --^-- P[16] = 1 -- 暴力计算,两次计算,更新 r
17 ^ P[17] = 0 -- 情况二,一次计算
18 ^ P[18] = 0 -- 终止

我列出了每次计算面对的情况,计算的次数以及是否需要 r 我希望大家思考 当新计算出的 r 与旧的 r 相等时,是否应该更新 center ? 当然不应该,我们肯定更倾向于选择更长的回文串

是这样吗? 我们思考一种情况

1
2
3
4
5
6
7
8
9
2       --^--                                p[2]  = 1
3 ^ P[3] = 0
4 ------^------ P[4] = 3
5 ^ P[5] = 0
6 ----------^---------- P[6] = 5
7 ^ P[7] = 0
8 ------^------ P[8] = 3
9 ^ P[9] = 0
10 --^-- P[10] = 1

第 6 行 较长有什么用呢,有用的只是 i 到 r 这一小截而已,不更新是因为都一样,没必要更新,所以只有当我们发现了更右边的 r 更新即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
class Solution {

private fun formatString(s: String): String{
val tString : StringBuffer = StringBuffer("^#")
for ( i in s.indices ){
tString.append(s[i])
tString.append('#')
}
return tString.append('$').toString()
}

private fun extend(s: String, leftIndex: Int, rightIndex: Int): Int {
var r = 0
var left = leftIndex
var right = rightIndex
while (left > 0 && right < s.length && s[left] == s[right]){
r ++
left --
right ++
}
return r
}

fun longestPalindrome(s: String): String {
val tString = formatString(s)
var maxR = 0
var maxCenter = 0
var center = -1
var R = 0
val p = Array<Int>(tString.length) { 0 }

for ( i in tString.indices ){
val iMirror = center - (i - center)
var rightIndex = i
var leftIndex = i
val maxLength = R - i

val hasMirrorIndex = i < R && center - maxLength > 0
val case1CompletelyIncluded = hasMirrorIndex && i > center && p[iMirror] < R - i
val case2NotCompletelyInclude = hasMirrorIndex && !case1CompletelyIncluded

if(!hasMirrorIndex){
p[i] = 0
rightIndex = i + 1
leftIndex = i - 1
} else {
if(case1CompletelyIncluded) {
p[i] = p[iMirror]
continue
} else if (case2NotCompletelyInclude) {
p[i] = maxLength
rightIndex = R + 1
leftIndex = i - (rightIndex - i)
}
}

p[i] += extend(tString, leftIndex, rightIndex)

// 更新最右边的 R 和 center
if(i + p[i] > R){
R = i + p[i]
center = i
}

// 判断是不是最长的回文串
if(p[i] > maxR){
maxR = p[i]
maxCenter = i
}
}

if(maxR == 0) return ""

val start = maxCenter - maxR
val end = maxCenter + maxR
return tString.substring(start..end).replace("#", "")
}
}

6. N 字形变换

题目

将一个给定字符串 s 根据给定的行数 numRows ,以从上往下、从左到右进行 Z 字形排列。

比如输入字符串为 "PAYPALISHIRING" 行数为 3 时,排列如下:

1
2
3
P   A   H   N
A P L S I I G
Y I R

之后,你的输出需要从左往右逐行读取,产生出一个新的字符串,比如:"PAHNAPLSIIGYIR"

请你实现这个将字符串进行指定行数变换的函数:

1
string convert(string s, int numRows);

示例 1:

1
2
输入:s = "PAYPALISHIRING", numRows = 3
输出:"PAHNAPLSIIGYIR"

示例 2:

1
2
3
4
5
6
7
输入:s = "PAYPALISHIRING", numRows = 4
输出:"PINALSIGYAHRPI"
解释:
P I N
A L S I G
Y A H R
P I

示例 3:

1
2
输入:s = "A", numRows = 1
输出:"A"

提示:

  • 1 <= s.length <= 1000
  • s 由英文字母(小写和大写)、',''.' 组成
  • 1 <= numRows <= 1000

简介

1
2
3
4
重点不在字形,重点在变换是什么变换
关键词:
一维函数变二维函数
往复变化函数

正文

我们思考,为什么这个算法难写
分析 N字形 的离散函数,我们面临着一个唯一的 x 对应着 1- N 个 y
x 变化的时候,我们首先要算该 x 对应了几个 y
这根本不符合函数的定义
函数是指一个集合中的每个元素都有且仅有一个映射到另一个集合中的元素,这种关系被称为函数映射

推出我们需要把一个一维度线性离线函数 s = f(n) 变成二维离散函数 s = g(x, y)
注意,这里好玩的是 y 和 x 其实是数列
即 s = g(x(n), y(n))
我们开始推导

1
2
3
4
5
6
7
8
9
10
11
12
x(n) = x(n-1) + 1
函数 x(n) 很简单,我们怎么表达 y(n) 呢
我们发现 y(n) 是一个往复的等差数列
对于本题有
y(n) = y(n - 1) + step(n)
step 是一个往复函数
用 flag 代表往复函数的方向
| -1 当 y(n - 1) 到达最大值或者最小值时
flag = | 1 其他
step(n) = flag * step(n-1) 当 y(n - 1) 到达最大值或者最小值时
| -step(n-1) 其他
step(n) = | step(n)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Solution {
fun convert(s: String, numRows: Int): String {
推出 (numRows < 2) return s
val rows: MutableList<StringBuilder> = ArrayList()
for (i in 0 until numRows) rows.add(StringBuilder())
var i = 0
var step = -1
for (c in s.toCharArray()) {
rows[i].append(c)
if (i == 0 || i == numRows - 1) step = -step
i += step
}
val res = StringBuilder()
for (row in rows) res.append(row)
return res.toString()
}
}

01/80 统计素数个数-暴力算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
fun isPrime(n: Int): Boolean {
if (n <= 1) return false
for (i in 2 until n) {
if (n % i == 0) {
return false
}
}
return true
}

fun countPrimesInArray(arr: IntArray): Int {
var count = 0
for (num in arr) {
if (isPrime(num)) {
count++
}
}
return count
}

fun main() {
val arr = intArrayOf(2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15)
println("Count of prime numbers in the array: ${countPrimesInArray(arr)}")
}