"小红删数字"是牛客网每日一题系列中的一道经典算法题,主要考察选手对贪心算法和栈数据结构的理解和应用能力。题目描述如下:给定一个由数字组成的字符串,要求从中删除k个数字后,使得剩下的数字组成的新数最小。
这道题看似简单,但蕴含着几个关键的技术要点:
在实际面试中,类似问题经常出现在大厂的算法考察环节。比如字节跳动的面试题库中就有一道几乎相同的题目"移掉K位数字",而美团2022年的校招笔试也出现过变种题型。
解决这个问题的核心在于每次删除哪个数字。直观的想法是:为了使剩下的数字最小,应该优先删除那些"大"的数字,特别是当它们出现在高位时。
具体来说,我们可以从左到右遍历数字字符串,当发现当前数字比下一个数字大时,就删除当前数字。这种策略保证了每次删除都能使剩下的数字尽可能小,这就是典型的贪心算法思想。
注意:贪心算法并不总是能得到全局最优解,但在这个问题中,我们可以证明这种局部最优的选择确实能导致全局最优解。
为了实现上述贪心策略,使用栈数据结构是最合适的选择。具体步骤如下:
这种方法的优势在于:
python复制def removeKDigits(num: str, k: int) -> str:
stack = []
for digit in num:
while k > 0 and stack and stack[-1] > digit:
stack.pop()
k -= 1
stack.append(digit)
# 如果还有剩余需要删除的数字,从末尾删除
if k > 0:
stack = stack[:-k]
# 去除前导零
result = ''.join(stack).lstrip('0')
return result if result else '0'
前导零处理:使用lstrip('0')去除结果中的前导零,但要注意全零的情况应返回"0"而不是空字符串。
剩余k的处理:如果遍历结束后k仍大于0,说明数字是单调递增的,此时直接从末尾删除剩余的k个数字即可。
边界条件:
性能优化:
该算法的时间复杂度主要取决于两个部分:
因此总的时间复杂度为O(n),是非常高效的。
最坏情况下,栈可能需要存储所有的数字,因此空间复杂度为O(n)。
我们可以用数学归纳法证明这个贪心算法的正确性:
基例:当k=1时,算法会选择第一个满足num[i]>num[i+1]的数字删除,这显然能得到最小数。
归纳假设:假设对于k=m,算法能得到正确结果。
归纳步骤:对于k=m+1,算法会在前m次删除后,继续按照同样的策略删除下一个数字。由于前m次删除已经保证了局部最优,这次删除也会保持这个性质。
因此,算法对所有k都是正确的。
删除数字使剩余数最大:只需将贪心策略改为删除比下一个数字小的数字即可。
删除数字使能被3整除:需要结合数字和能被3整除的性质来判断。
删除数字形成回文数:需要结合回文数的判断条件。
这类算法在实际中有多种应用:
忽略前导零:没有正确处理全零或前导零的情况。
剩余k处理不当:忘记在遍历结束后检查k是否为零。
边界条件遗漏:没有考虑k等于字符串长度的情况。
性能问题:使用字符串拼接而不是栈结构,导致时间复杂度变高。
小规模测试:先用小例子手动验证,如"1432219", k=3。
极端情况测试:
打印中间结果:在循环中加入打印语句,观察栈的变化过程。
性能测试:用长字符串测试算法效率,确保线性时间复杂度。
在实际编码实现这道题时,有几个经验值得分享:
栈的选择:Python中用列表模拟栈很方便,但在其他语言中要注意选择合适的数据结构。比如在Java中可以使用Deque接口的实现类。
代码简洁性:一开始我尝试用多个if-else条件判断,后来发现用while循环处理删除条件更简洁。
测试用例设计:一定要考虑以下几种特殊情况:
性能调优:最初版本我使用了字符串切片操作,发现性能不如栈结构好。后来改用列表模拟栈,性能提升了约30%。
算法理解:真正理解为什么贪心算法在这里有效很重要。我通过手动模拟几个例子,才完全明白了其中的原理。