贪心算法 (Greedy Algorithm):这是一种在每一步选择中都采取在当前状态下最好或最优(即最有利)的选择,从而希望得到全局最优解的算法思想。
要用最多一次交换 得到最大值,我们的目标非常明确:用右侧最大的数字,去替换左侧第一个比它小的数字。
核心思路:贪心 + 定位
我们希望让高位 (最左边)的数字尽可能大。所以,我们应该从左向右遍历,找到第一个可以被交换的低位数字。
步骤一:找到第一个需要交换的位置 (左侧目标 iii)
从左到右扫描数字。如果当前位 iii 的数字小于它右侧的任何 数字,那么这一位就是可以被交换的位置。我们找到的第一个 这样的位置 iii 就是我们的左侧目标。
如果整个数字都是降序或相等(例如 987 或 999),则不需要交换。
步骤二:找到右侧最大的替换数字 (右侧目标 jjj)
确定了左侧目标 iii 后,我们需要在 iii 的右侧(即 [i+1,n−1][i+1, n-1][i+1,n−1] 范围内)找到:
- 值最大的数字。
- 如果有多个最大值,我们选择最靠左 的那个(即索引最大的 jjj),这样可以保证交换后的数字排列是最大的。
注意: 我们要找的是最靠右 的最大的替换数字 jjj,因为:
- 如果数字是
1993
,i=0i=0i=0。右侧有两个 9。如果交换1
和最左边 的 9 得到9193
。 - 如果交换
1
和最右边 的 9 得到9913
。
显然,交换最靠右的那个最大数字得到的结果更大。
步骤三:执行交换
将位置 iii 和位置 jjj 上的数字进行交换,得到最终的最大值。
Python 代码实现
python
class Solution:
def maximumSwap(self, num: int) -> int:
# 将整数转换为字符列表
s = list(str(num))
n = len(s)
# --- 步骤 1: 预处理 - 从右向左扫描,构建右侧最佳索引表 ---
# right_best_idx
# right_best_idx[i] 存储的是:在 s[i:] 区间内,最大数字所在的"最靠右"索引 j (j >= i)
# 换句话说,它是给 s[i] 准备的"最佳替换目标"的索引。
right_best_idx = [0] * n
# current_max_idx
# current_max_idx 实时追踪当前已扫描区域内最大值的索引
current_max_idx = n - 1
# 从右向左遍历 (i 从 n-1 递减到 0)
for i in range(n - 1, -1, -1):
# 如果 s[i] 大于或等于当前最大值 s[current_max_idx],
# 则更新 current_max_idx 为 i,确保追踪的是最靠左的那个最大数字的索引
if s[i] >= s[current_max_idx]:
current_max_idx = i
# 记录当前位置 i 的"最佳替换目标" (即当前区域的最大数字的位置)
right_best_idx[i] = current_max_idx
# --- 步骤 2 & 3: 寻找交换对 (i, j) 并执行交换 ---
# 从左向右遍历 (i 是左侧的目标位置)
for i in range(n):
# j 是通过查表获得的右侧最佳替换位置
j = right_best_idx[i]
# 如果 s[i] 小于其右侧最大数字 s[j],则找到交换对
# s[i] 是第一个需要优化的低位,s[j] 是右侧能找到的最好增益
if s[i] < s[j]:
# 执行交换
s[i], s[j] = s[j], s[i]
# 交换一次后立即返回
return int("".join(s))
# 如果没有找到交换机会,返回原数字
return num
它涉及到两个独立的循环 和两个不同的目标。
为什么第一步(预处理)要在 SSS 的右侧操作,而不是直接在最左侧(S[0]S[0]S[0])操作?
核心区分:第一步(预处理)不是交换!
请务必区分代码中的两个 O(N)O(N)O(N) 循环所承担的不同职责:
循环/步骤 | 遍历方向 | 核心目标 | 为什么不直接在左侧 S[0]S[0]S[0] 操作? |
---|---|---|---|
第一步 (预处理) | 从右向左 (i=n−1→0i = n-1 \to 0i=n−1→0) | 目的:构建表格 (right_best_idx )。不执行交换。 |
必须为所有可能的 iii 准备答案。 iii 可能是 0,1,2,...0, 1, 2, \dots0,1,2,... 中的任意一个,所以必须为所有位置计算右侧的最佳替换位。 |
第二步 (查找交换) | 从左向右 (i=0→n−1i = 0 \to n-1i=0→n−1) | 目的:执行贪心决策 ,找到第一个 需要优化的 iii。 | 执行交换。 这是唯一需要优先考虑 i=0i=0i=0 的地方,因为它是最高位。 |
你提到的这个第一个 for
循环是整个算法的核心预处理步骤,它的功能是:
为每一位数字,找到它右边所有数字中"最大数字"的索引位置(如果有多个相同最大数字,则取最靠右的那个)。
🔍 代码片段回顾
python
right_best_idx = [0] * n
current_max_idx = n - 1
for i in range(n - 1, -1, -1):
if s[i] >= s[current_max_idx]:
current_max_idx = i
right_best_idx[i] = current_max_idx
📘 直观理解
举个例子,比如数字:
num = 2736
s = ['2', '7', '3', '6']
我们要知道:
- 每个位置 右边 哪个数字是"最大且最靠右"的。
从右往左扫描过程:
i | s[i] | 当前最大数字索引 current_max_idx | right_best_idx[i] |
---|---|---|---|
3 | 6 | 3 | 3 |
2 | 3 | 3 | 3 |
1 | 7 | 1 | 1 |
0 | 2 | 1 | 1 |
结果:
python
right_best_idx = [1, 1, 3, 3]
含义:
- 对于位置 0(数字2),右边的最大数在索引 1(数字7)
- 对于位置 1(数字7),右边的最大数在索引 1(数字7本身)
- 对于位置 2(数字3),右边的最大数在索引 3(数字6)
- 对于位置 3(数字6),右边的最大数在索引 3(数字6本身)
⚙️ 这个循环的目的是什么?
它在为每一位数字准备信息:
如果我要"最大化"这个数字,通过一次交换,我应该和谁换?
也就是说,right_best_idx[i]
告诉我们:
在
i
右边所有数字里,哪个是最大的(且最靠右)数字。
💡 为什么要从右往左扫描?
因为我们需要在每一步都知道 右边区域中最大的数字 ,
而从右向左扫描时,可以"实时更新"右侧最大数字的索引 current_max_idx
。
这样时间复杂度是 O(n),非常高效。
🧠 逻辑总结一句话:
第一个 for 循环的功能是------为每个位置 i,找出它右边(包括自己)中最大的数字所在的索引。这样在第二个 for 循环中,我们只需 O(1) 时间就能知道"右边最有潜力交换的那个数字",从而高效地完成"最大交换"操作。
第一个 for
循环的预处理作用------为每个位置找出右边"最优交换目标"。
现在我们来看 第二个 for
循环 是如何利用这个结果来实现"最大交换"的。
🧩 第二个 for 循环的核心逻辑
代码如下:
python
for i in range(n):
j = right_best_idx[i]
if s[i] < s[j]:
s[i], s[j] = s[j], s[i]
return int("".join(s))
return num
🔍 它在干什么?
它从左到右 依次检查每一位数字 s[i]
:
- 看看在右边有没有一个比它大的数字(通过
right_best_idx[i]
直接查到); - 如果有,立刻交换;
- 因为我们是从左到右找的,所以第一次能交换的地方 就是能让整个数增大最多的位置;
- 一旦交换完,就返回结果。
🔧 结合第一个循环的结果理解
我们前面得到了:
python
right_best_idx[i] = 在 s[i:] 右侧区间中,最大数字的最靠右索引
这意味着:
在第 i
位上,如果 s[i] < s[right_best_idx[i]]
,那就存在一个比它大的数在后面。
而我们只允许交换一次,所以我们就应该:
- 选择 最左边 的那个可以变大的位置
i
- 和 最右边 的那个最大数字位置
j
交换
这样能保证结果最大。
📘 举个完整例子:num = 2736
第一步:预处理结果(来自第一个 for 循环)
python
s = ['2', '7', '3', '6']
right_best_idx = [1, 1, 3, 3]
第二步:逐位检查并决定交换
i | s[i] | j = right_best_idx[i] | s[j] | 比较结果 | 操作 |
---|---|---|---|---|---|
0 | 2 | 1 | 7 | 2 < 7 ✅ | 交换 → ['7','2','3','6'],返回 |
1 | 7 | 1 | 7 | 7 == 7 ❌ | - |
2 | 3 | 3 | 6 | 3 < 6 ✅(但不会执行,因为上面已返回) | - |
最终结果:
7236
⚙️ 为什么"从左往右"找?
因为我们想要最大化最终结果 。
数的左边位权更高(比如千位 > 百位 > 十位),
所以:
- 第一个能换大的位置,就是提升最大的地方;
- 一旦找到,就不用再往后看了。
🧠 总结一句话:
第二个
for
循环利用第一个循环构建的"右侧最大值索引表",快速判断每个位置是否能通过一次交换变大;一旦找到第一个可以增大的位置,就与最右的最大数字交换,从而保证结果最大化。
画一个可视化的流程图,让你直观理解这两个循环是如何协同工作的。
示例:输入 num = 2736
┌────────────────────────────────────┐
│ Step 1:从右向左扫描(建立右侧最大值表) │
└────────────────────────────────────┘
索引: 0 1 2 3
数字: 2 7 3 6
从右往左扫描构建 right_best_idx
:
i=3: 当前最大值 = 6 → right_best_idx[3] = 3
i=2: s[2]=3 < 6 → right_best_idx[2] = 3
i=1: s[1]=7 >= 6 → 更新最大值索引=1 → right_best_idx[1] = 1
i=0: s[0]=2 < 7 → right_best_idx[0] = 1
结果:
right_best_idx = [1, 1, 3, 3]
图形表示👇:
索引: 0 1 2 3
数字: 2 7 3 6
指向: ↓ ↓ ↓ ↓
最大值: 7 7 6 6
(每个箭头指向"右侧区域中最大且最靠右"的数字)
┌─────────────────────────────────────────────┐
│ Step 2:从左向右扫描(寻找第一个可交换位置) │
└─────────────────────────────────────────────┘
扫描每个位置:
i | s[i] | right_best_idx[i] | s[j] | 判断 | 动作 |
---|---|---|---|---|---|
0 | 2 | 1 | 7 | 2 < 7 ✅ | 交换(0,1) → 7236,结束 |
1 | 7 | 1 | 7 | - | - |
2 | 3 | 3 | 6 | 3 < 6(不会执行,因为前面已返回) | - |
执行过程可视化👇:
初始: 2 7 3 6
↑ ↑
i=0 j=1
→ 交换 → 7 2 3 6
输出:7236
🧠 全流程逻辑图总结
┌────────────────────┐
│ 输入 num → 转字符列表 │
└───────┬────────────┘
│
▼
┌────────────────────┐
│ 从右向左构建 right_best_idx │
└───────┬────────────┘
│
▼
┌────────────────────────────┐
│ 从左向右扫描 i: │
│ j = right_best_idx[i] │
│ 若 s[i] < s[j] → 交换并返回 │
└────────────────────────────┘
│
▼
┌────────────────────────────┐
│ 若未找到可交换位置 → 返回原数 │
└────────────────────────────┘
这句代码 return int("".join(s))
是 Python 中将字符列表 (在算法中我们用来表示数字)转换回最终整数结果的经典且高效的写法。
我将用中文为您拆解它的含义和功能。
1. 代码的含义和功能拆解
这句代码可以分解为三个连续的操作,是从内到外依次执行的:
操作 | 代码片段 | 英文词源 & 解释 | 功能 |
---|---|---|---|
第一步 (内层) | "".join(s) |
join (连接) |
将列表 s 中所有的字符元素 ,使用最前面的空字符串 "" 连接起来,形成一个完整的字符串。 |
第二步 (中层) | int(...) |
int (Integer, 整数) |
将第一步得到的字符串 (如 "7236" )整体转换为一个整数。 |
第三步 (外层) | return ... |
return (返回) |
将第二步得到的整数值作为函数或方法的最终结果返回。 |
2. 代码的功能和在 LCA 中的作用
A. 功能:高效的类型转换
这行代码实现了从 List of Characters →\to→ String →\to→ Integer 的两次类型转换。
转换前 | 示例 | 转换后 |
---|---|---|
字符列表 s |
['7', '2', '3', '6'] |
→""join(s)\xrightarrow{\text{""join(s)}}""join(s) |
字符串 "7236" |
→int(...)\xrightarrow{\text{int(...)}}int(...) | 整数 7236 |
B. 在"最大交换"算法中的作用
在"最大交换"的贪心算法中,我们为了方便地执行交换操作 ,不得不将输入的整数 num
转换为字符列表 s
:
python
s = list(str(num)) # 例如:2736 -> ['2', '7', '3', '6']
# ... 执行交换操作 s[i], s[j] = s[j], s[i] ...
一旦我们找到了最优的交换对并执行了交换(此时 s
列表已经被修改为最大值形态,如 ['7', '2', '3', '6']
),我们就必须将这个最终结果变回题目的要求格式:一个非负整数。
因此,return int("".join(s))
就是将算法中处理好的字符列表 变回整数并输出的最终步骤。