C#数组与算法实战:从基础遍历到滑动窗口的深度进阶指南

如果你刚开始接触C#,或者已经写过一些控制台应用,但总觉得对数组、字符串这类基础数据结构的操作停留在“会用”层面,那么这篇文章正是为你准备的。我见过不少开发者,能熟练写出for循环求和,但一旦遇到“滑动窗口最大值”这类问题,思路就卡住了。这背后的原因,往往不是算法本身多难,而是缺乏一套从基础操作到高级模式平滑过渡的实战训练体系。

本文将带你跳出孤立练习的陷阱,通过一组精心设计的实战案例,串联起C#数组操作的核心技能树。我们不止步于写出能运行的代码,更会深入探讨为何这样设计不同方法间的性能差异以及如何将这些技巧应用到真实项目场景中。无论是准备技术面试,还是希望提升日常编码的健壮性与效率,这里的22个练习都将成为你坚实的垫脚石。

1. 夯实基础:数组操作的四种核心模式

在深入任何复杂算法之前,我们必须对数组这一最基本的数据结构了如指掌。C#中的数组不仅是数据的容器,更是理解内存访问、迭代逻辑和算法效率的起点。

1.1 遍历与聚合:超越简单的循环

求和、求积、求平均值,这些操作看似初级,却隐藏着编写高质量代码的关键:边界处理溢出预防选择正确的数据类型

// 一个健壮的数组求和示例
public static long RobustArraySum(int[] numbers)
{
    if (numbers == null) throw new ArgumentNullException(nameof(numbers));
    if (numbers.Length == 0) return 0; // 空数组的和定义为0

    long total = 0; // 使用long防止大数求和溢出
    foreach (int num in numbers)
    {
        // 这里可以加入业务逻辑,例如过滤无效值
        // if (num == int.MinValue) continue; // 示例:跳过特定值
        total += num;
    }
    return total;
}

注意:对于大规模数据求和,务必使用long甚至BigInteger类型。int类型的最大值约为21亿,很容易在累加过程中溢出,且不抛出异常,导致难以察觉的逻辑错误。

单纯计算聚合值意义有限,真正的价值在于将聚合模式抽象出来。C#的LINQ提供了Sum()Average()等方法,但理解其背后的手动实现,能让你在无法使用LINQ的环境(如某些高性能或受限场景)或需要自定义聚合逻辑时游刃有余。

让我们对比几种常见聚合操作的性能考量:

操作类型关键考虑点常见陷阱推荐做法
求和/求积数据类型溢出、空数组处理使用int累加可能溢出使用longchecked关键字
求平均值整数除法的精度丢失int / int 结果仍是int使用double类型进行除法
查找最值数组为空的处理假设数组非空直接取arr[0]先判空,或使用int.MinValue/int.MaxValue初始化

1.2 查找与筛选:效率与可读性的平衡

查找数组中的偶数,或筛选满足某个条件的所有元素,是日常编程中最频繁的操作之一。这里我们面临一个选择:返回新数组还是修改原数组?

// 方法1:使用List<T>作为缓冲,返回新数组(通用性好)
public static int[] FilterEvensWithList(int[] source)
{
    var evensList = new List<int>();
    foreach (var num in source)
    {
        if (num % 2 == 0) evensList.Add(num);
    }
    return evensList.ToArray(); // 分配新数组
}

// 方法2:使用yield return实现惰性求值(内存效率高)
public static IEnumerable<int> FilterEvensLazy(int[] source)
{
    foreach (var num in source)
    {
        if (num % 2 == 0) yield return num;
    }
}

第一种方法意图明确,结果立即可用,但需要额外内存分配。第二种方法采用了迭代器,只有在真正需要元素时才计算,适合处理大型数据集或链式操作。在实际项目中,我通常根据调用方的需求来决定:如果结果需要被多次访问或随机存取,用第一种;如果只是作为中间结果进行进一步处理,第二种更优。

查找最大值/最小值时,一个容易被忽略的优化是同时查找。在一次遍历中完成两项工作,虽然对现代CPU的流水线影响不大,但体现了减少迭代次数的思维。

public static (int Max, int Min) FindMaxAndMinInOnePass(int[] arr)
{
    if (arr == null || arr.Length == 0)
        throw new ArgumentException("数组不能为空");

    int max = arr[0];
    int min = arr[0];
    // 从第二个元素开始遍历
    for (int i = 1; i < arr.Length; i++)
    {
        if (arr[i] > max) max = arr[i];
        else if (arr[i] < min) min = arr[i]; // 用else if,因为一个数不可能同时大于max又小于min
    }
    return (max, min);
}

2. 字符串处理:不可变性的艺术与实战

C#中的字符串是不可变的(immutable)。这意味着任何修改操作(如拼接、替换)都会产生新的字符串对象。理解这一点,是写出高效字符串处理代码的关键。

2.1 反转、回文与去重:理解字符数组

字符串反转是最经典的面试题之一,它至少有四种常见实现方式,各有优劣:

// 方式1:使用Array.Reverse (最简洁)
public static string ReverseWithArray(string input)
{
    char[] charArray = input.ToCharArray();
    Array.Reverse(charArray);
    return new string(charArray);
}

// 方式2:使用StringBuilder (适合多次修改)
public static string ReverseWithStringBuilder(string input)
{
    var sb = new StringBuilder(input.Length);
    for (int i = input.Length - 1; i >= 0; i--)
    {
        sb.Append(input[i]);
    }
    return sb.ToString();
}

// 方式3:使用栈 (体现数据结构思维)
public static string ReverseWithStack(string input)
{
    var stack = new Stack<char>();
    foreach (char c in input) stack.Push(c);
    return new string(stack.ToArray());
}

// 方式4:原地交换 (性能最优,但代码稍复杂)
public static string ReverseInPlace(string input)
{
    char[] chars = input.ToCharArray();
    int left = 0, right = chars.Length - 1;
    while (left < right)
    {
        // 交换字符
        (chars[left], chars[right]) = (chars[right], chars[left]);
        left++;
        right--;
    }
    return new string(chars);
}

对于回文判断,核心在于双指针技巧。但实际应用中,我们往往需要忽略大小写和标点符号。下面是一个工业级强度的回文检查器:

public static bool IsPalindromeRobust(string s)
{
    if (string.IsNullOrEmpty(s)) return true;

    int left = 0, right = s.Length - 1;
    while (left < right)
    {
        // 跳过非字母数字字符
        while (left < right && !char.IsLetterOrDigit(s[left])) left++;
        while (left < right && !char.IsLetterOrDigit(s[right])) right--;

        // 比较(忽略大小写)
        if (char.ToLowerInvariant(s[left]) != char.ToLowerInvariant(s[right]))
            return false;

        left++;
        right--;
    }
    return true;
}

字符串去重看似简单,但如何保留首次出现的顺序?HashSet<T>是解决这个问题的利器,它能提供O(1)时间复杂度的查找。

public static string RemoveDuplicateChars(string input)
{
    if (string.IsNullOrEmpty(input)) return input;

    var seen = new HashSet<char>();
    var result = new StringBuilder();

    foreach (char c in input)
    {
        if (seen.Add(c)) // HashSet.Add 返回bool,表示是否成功添加(即是否为新元素)
        {
            result.Append(c);
        }
    }
    return result.ToString();
}

2.2 子串、压缩与公共前缀:滑动窗口的雏形

“最长无重复字符子串”是LeetCode上的经典题目,也是滑动窗口算法的绝佳入门案例。其核心思想是维护一个不包含重复字符的窗口,通过左右指针的移动来寻找最大窗口。

public static int LengthOfLongestSubstring(string s)
{
    if (string.IsNullOrEmpty(s)) return 0;

    // 字典存储字符最近一次出现的位置
    var charIndex = new Dictionary<char, int>();
    int maxLength = 0;
    int windowStart = 0; // 窗口左边界

    for (int windowEnd = 0; windowEnd < s.Length; windowEnd++)
    {
        char currentChar = s[windowEnd];

        // 如果字符已在窗口中,需要收缩左边界
        if (charIndex.ContainsKey(currentChar))
        {
            // 关键:窗口左边界只能向右移动,不能向左
            windowStart = Math.Max(windowStart, charIndex[currentChar] + 1);
        }

        // 更新字符的最新位置
        charIndex[currentChar] = windowEnd;
        // 计算当前窗口长度
        maxLength = Math.Max(maxLength, windowEnd - windowStart + 1);
    }

    return maxLength;
}

这个算法的精妙之处在于windowStart = Math.Max(windowStart, charIndex[currentChar] + 1)这一行。它确保了窗口左边界不会回退,从而保证了O(n)的时间复杂度。理解这个逻辑,就掌握了滑动窗口算法的精髓。

字符串压缩(如将"aabcccccaaa"压缩为"a2b1c5a3")则考验着状态维护边界处理的能力。这里需要注意,如果压缩后的字符串没有变短,应返回原字符串。

public static string CompressString(string str)
{
    if (string.IsNullOrEmpty(str) || str.Length <= 1) 
        return str;

    var compressed = new StringBuilder();
    int countConsecutive = 1;

    for (int i = 1; i <= str.Length; i++) // 注意:这里 i <= length,为了处理最后一个字符
    {
        // 如果当前字符与前一个相同,计数增加
        if (i < str.Length && str[i] == str[i - 1])
        {
            countConsecutive++;
        }
        else
        {
            // 追加前一个字符及其计数
            compressed.Append(str[i - 1]);
            compressed.Append(countConsecutive);
            countConsecutive = 1; // 重置计数
        }
    }

    // 只有压缩后更短才返回压缩结果
    return compressed.Length < str.Length ? compressed.ToString() : str;
}

3. 算法思维进阶:从哈希映射到双指针

当基础操作熟练后,我们需要引入更高级的算法思维来解决实际问题。哈希表(在C#中常用Dictionary<TKey, TValue>)和双指针是其中最常用的两种工具。

3.1 哈希表的妙用:两数之和与频率统计

“两数之和”问题(给定数组和目标值,找出和为目标值的两个数的索引)是哈希表应用的典范。暴力解法需要O(n²)的时间,而哈希表可以将其优化到O(n)。

public static (int index1, int index2)? FindTwoSumIndices(int[] nums, int target)
{
    if (nums == null || nums.Length < 2)
        return null;

    // 键:数值,值:该数值的索引
    var numMap = new Dictionary<int, int>();

    for (int i = 0; i < nums.Length; i++)
    {
        int complement = target - nums[i];
        
        // 检查补数是否已在字典中
        if (numMap.TryGetValue(complement, out int complementIndex))
        {
            return (complementIndex, i); // 找到一对
        }
        
        // 将当前数及其索引加入字典
        // 注意:先检查再添加,避免同一个元素使用两次
        if (!numMap.ContainsKey(nums[i]))
        {
            numMap[nums[i]] = i;
        }
    }
    
    return null; // 未找到
}

这个算法的关键在于边遍历边构建查找表。对于每个元素,我们计算其“补数”(即target - current),然后查看这个补数是否已经出现过。如果出现过,我们就找到了一对解。这种“用空间换时间”的策略在算法设计中非常普遍。

哈希表的另一个常见用途是统计频率。例如,找出数组中出现次数超过一半的元素(众数):

public static int? FindMajorityElement(int[] nums)
{
    if (nums == null || nums.Length == 0) return null;
    
    var frequency = new Dictionary<int, int>();
    int majorityThreshold = nums.Length / 2;
    
    foreach (int num in nums)
    {
        // 更新频率计数
        frequency[num] = frequency.ContainsKey(num) ? frequency[num] + 1 : 1;
        
        // 如果某个元素的计数已超过阈值,直接返回
        if (frequency[num] > majorityThreshold)
            return num;
    }
    
    return null; // 没有众数
}

3.2 双指针技巧:快慢指针与左右指针

双指针技巧主要分为两类:快慢指针(常用于链表)和左右指针(常用于数组)。在数组操作中,左右指针尤为常见。

一个经典例子是移除有序数组中的重复项,要求原地修改并返回新长度:

public static int RemoveDuplicatesFromSortedArray(int[] nums)
{
    if (nums == null || nums.Length == 0) return 0;
    
    int uniqueIndex = 0; // 慢指针:指向最后一个唯一元素的位置
    
    for (int current = 1; current < nums.Length; current++) // 快指针:遍历数组
    {
        if (nums[current] != nums[uniqueIndex])
        {
            uniqueIndex++;
            nums[uniqueIndex] = nums[current]; // 将新唯一元素移到前面
        }
    }
    
    return uniqueIndex + 1; // 新长度为索引+1
}

这个算法的核心是维护两个指针:uniqueIndex(慢)和current(快)。快指针遍历整个数组,慢指针标记唯一序列的末尾。当快指针遇到与慢指针不同的元素时,就将该元素复制到慢指针的下一个位置,然后慢指针前进。这样,所有唯一元素都被紧凑地排列在数组开头。

另一个双指针的变体是夹逼法,常用于在有序数组中寻找两个数的和或差。虽然我们的“两数之和”问题数组是无序的,但如果是已排序数组,可以有更优解:

// 假设数组已按升序排序
public static (int left, int right)? FindTwoSumInSortedArray(int[] sortedNums, int target)
{
    if (sortedNums == null || sortedNums.Length < 2)
        return null;
    
    int left = 0;
    int right = sortedNums.Length - 1;
    
    while (left < right)
    {
        int sum = sortedNums[left] + sortedNums[right];
        
        if (sum == target)
            return (left, right);
        else if (sum < target)
            left++; // 和太小,左指针右移以增加和
        else
            right--; // 和太大,右指针左移以减少和
    }
    
    return null;
}

这种方法的时间复杂度是O(n),且不需要额外空间(哈希表需要O(n)空间)。它充分利用了数组有序的特性,通过调整左右指针来逼近目标值。

4. 高级模式:滑动窗口与矩阵操作

当问题涉及“连续子数组”或“固定大小窗口”时,滑动窗口算法往往是最优解。而矩阵操作则考验着我们对二维数据结构的理解。

4.1 滑动窗口最大值:双端队列的威力

“滑动窗口最大值”是滑动窗口算法的经典难题。暴力解法对每个窗口都重新查找最大值,时间复杂度为O(n*k),其中n是数组长度,k是窗口大小。使用双端队列(Deque)可以优化到O(n)。

public static int[] MaxSlidingWindow(int[] nums, int k)
{
    if (nums == null || k <= 0 || nums.Length < k)
        return Array.Empty<int>();
    
    // 结果数组,长度为 n - k + 1
    int[] result = new int[nums.Length - k + 1];
    int resultIndex = 0;
    
    // 双端队列,存储的是索引(不是值),且队列中的索引对应的值是递减的
    var deque = new LinkedList<int>();
    
    for (int i = 0; i < nums.Length; i++)
    {
        // 步骤1:移除队列中不在当前窗口范围内的索引
        // 窗口范围是 [i-k+1, i],所以索引小于 i-k+1 的元素都不在窗口内
        while (deque.Count > 0 && deque.First.Value < i - k + 1)
            deque.RemoveFirst();
        
        // 步骤2:维护队列的递减性
        // 从队列尾部开始,移除所有小于当前元素的索引
        while (deque.Count > 0 && nums[deque.Last.Value] < nums[i])
            deque.RemoveLast();
        
        // 步骤3:将当前索引加入队列
        deque.AddLast(i);
        
        // 步骤4:当窗口形成后(i >= k-1),记录当前窗口的最大值
        // 队列头部始终是当前窗口最大值的索引
        if (i >= k - 1)
        {
            result[resultIndex++] = nums[deque.First.Value];
        }
    }
    
    return result;
}

这个算法的精妙之处在于双端队列的维护策略:

  1. 队列中存储的是索引,而不是值,这样我们可以判断索引是否还在窗口内
  2. 队列中的索引对应的数组值是递减的,这样队列头部就是当前窗口的最大值
  3. 添加新元素时,从队列尾部移除所有小于它的元素,保持递减性

理解这个算法后,你可以解决一系列滑动窗口问题,如“滑动窗口最小值”、“大小为K的子数组的最大和”等。

4.2 矩阵旋转:分层处理与坐标映射

矩阵旋转是面试中常见的问题,特别是顺时针旋转90度。最直观的方法是创建新矩阵,但题目往往要求原地旋转(不占用额外空间)。

public static void RotateMatrixClockwise(int[,] matrix)
{
    int n = matrix.GetLength(0); // 假设是n x n矩阵
    
    // 分层旋转:从外圈到内圈
    for (int layer = 0; layer < n / 2; layer++)
    {
        int first = layer;
        int last = n - 1 - layer;
        
        for (int i = first; i < last; i++)
        {
            int offset = i - first;
            
            // 保存上边
            int top = matrix[first, i];
            
            // 左到上
            matrix[first, i] = matrix[last - offset, first];
            
            // 下到左
            matrix[last - offset, first] = matrix[last, last - offset];
            
            // 右到下
            matrix[last, last - offset] = matrix[i, last];
            
            // 上到右(使用保存的top)
            matrix[i, last] = top;
        }
    }
}

这个算法的关键是找到旋转时元素的对应关系。对于位置(row, col)的元素,顺时针旋转90度后的新位置是(col, n-1-row)。分层处理则让我们可以逐圈旋转,而不需要复杂的坐标计算。

为了更直观地理解,我们可以看一个3x3矩阵的旋转过程:

初始矩阵:
1 2 3
4 5 6
7 8 9

旋转过程(外层):
保存左上角1
7 -> 左上角 (0,0)
9 -> (0,2)的位置
3 -> (2,2)的位置
1 -> (2,0)的位置(但实际是放到(0,2)?需要仔细跟踪)

实际上,对于四角:
temp = 左上角(1)
左上角(1) = 左下角(7)
左下角(7) = 右下角(9)
右下角(9) = 右上角(3)
右上角(3) = temp(1)

除了旋转,矩阵的转置也是常见操作:

public static void TransposeMatrix(int[,] matrix)
{
    int rows = matrix.GetLength(0);
    int cols = matrix.GetLength(1);
    
    // 转置:matrix[i,j] 与 matrix[j,i] 交换
    for (int i = 0; i < rows; i++)
    {
        for (int j = i + 1; j < cols; j++) // 从i+1开始,避免重复交换
        {
            // 交换元素
            int temp = matrix[i, j];
            matrix[i, j] = matrix[j, i];
            matrix[j, i] = temp;
        }
    }
}

矩阵转置后再反转每一行,就得到了顺时针旋转90度的结果。这种方法可能更直观,但需要理解矩阵操作的本质。

5. 综合实战:算法思维的融合应用

掌握了单个技巧后,真正的挑战在于如何将它们组合起来解决复杂问题。让我们看几个综合性的例子。

5.1 有效的括号序列:栈的经典应用

检查括号序列是否有效(如"()[]{}"有效,"(]"无效)是栈数据结构的教科书式应用。但实际面试中,问题往往会有所扩展。

基础版本使用栈:

public static bool IsValidParentheses(string s)
{
    if (string.IsNullOrEmpty(s)) return true;
    
    var stack = new Stack<char>();
    var mapping = new Dictionary<char, char>
    {
        { ')', '(' },
        { ']', '[' },
        { '}', '{' }
    };
    
    foreach (char c in s)
    {
        if (mapping.ContainsValue(c)) // 开括号
        {
            stack.Push(c);
        }
        else if (mapping.ContainsKey(c)) // 闭括号
        {
            if (stack.Count == 0 || stack.Peek() != mapping[c])
                return false;
            stack.Pop();
        }
        // 其他字符可以忽略或根据需求处理
    }
    
    return stack.Count == 0; // 栈应为空
}

扩展问题1:支持多种括号类型。上面的代码已经通过字典实现了这一点,添加新的括号类型只需更新字典。

扩展问题2:找出最长有效括号子串。这个问题就复杂多了,需要动态规划或栈的巧妙运用:

public static int LongestValidParentheses(string s)
{
    if (string.IsNullOrEmpty(s)) return 0;
    
    int maxLength = 0;
    var stack = new Stack<int>();
    stack.Push(-1); // 哨兵值,方便计算长度
    
    for (int i = 0; i < s.Length; i++)
    {
        if (s[i] == '(')
        {
            stack.Push(i);
        }
        else // s[i] == ')'
        {
            stack.Pop();
            if (stack.Count == 0)
            {
                // 栈为空,将当前位置作为新的基准
                stack.Push(i);
            }
            else
            {
                // 计算当前有效长度
                maxLength = Math.Max(maxLength, i - stack.Peek());
            }
        }
    }
    
    return maxLength;
}

这个算法中,栈存储的是索引。遇到'('时压入索引,遇到')'时弹出。栈底始终保持一个"基准"索引,用于计算有效括号的长度。当栈被弹空时,说明当前的')'没有匹配的'(',于是将其索引作为新的基准。

5.2 数字处理与边界情况

数字到字符串的转换、阶乘计算等问题看似简单,但隐藏着许多边界情况和陷阱。

阶乘计算不仅要处理负数,还要考虑溢出大数问题:

public static BigInteger Factorial(int n)
{
    if (n < 0)
        throw new ArgumentException("阶乘未定义于负整数");
    if (n == 0 || n == 1)
        return 1;
    
    // 使用BigInteger处理大数
    BigInteger result = 1;
    for (int i = 2; i <= n; i++)
    {
        result *= i;
    }
    return result;
}

对于数字转英文单词,我们需要处理各种边界情况:零、负数、大数(百万、十亿等)。下面是一个支持0到999,999,999的版本:

private static readonly string[] Ones = 
    { "", "One", "Two", "Three", "Four", "Five", "Six", "Seven", "Eight", "Nine" };
private static readonly string[] Teens = 
    { "Ten", "Eleven", "Twelve", "Thirteen", "Fourteen", "Fifteen", 
      "Sixteen", "Seventeen", "Eighteen", "Nineteen" };
private static readonly string[] Tens = 
    { "", "", "Twenty", "Thirty", "Forty", "Fifty", 
      "Sixty", "Seventy", "Eighty", "Ninety" };

public static string NumberToWords(int num)
{
    if (num == 0) return "Zero";
    if (num < 0) return "Negative " + NumberToWords(-num);
    
    return Convert(num).Trim();
}

private static string Convert(int num)
{
    if (num >= 1000000000) // 十亿
        return Convert(num / 1000000000) + " Billion " + Convert(num % 1000000000);
    else if (num >= 1000000) // 百万
        return Convert(num / 1000000) + " Million " + Convert(num % 1000000);
    else if (num >= 1000) // 千
        return Convert(num / 1000) + " Thousand " + Convert(num % 1000);
    else if (num >= 100) // 百
        return Convert(num / 100) + " Hundred " + Convert(num % 100);
    else if (num >= 20) // 20-99
        return Tens[num / 10] + " " + Convert(num % 10);
    else if (num >= 10) // 10-19
        return Teens[num - 10];
    else // 1-9
        return Ones[num];
}

这个实现使用了递归,将大数分解为更小的部分。注意处理空格和零的情况,确保输出格式正确。

5.3 最长公共前缀:分治与二分查找

找出一组字符串的最长公共前缀,最简单的方法是纵向扫描,但还有更高效的方法。

纵向扫描法:

public static string LongestCommonPrefixVertical(string[] strs)
{
    if (strs == null || strs.Length == 0) return "";
    
    // 以第一个字符串为基准
    for (int i = 0; i < strs[0].Length; i++)
    {
        char currentChar = strs[0][i];
        
        // 检查其他字符串在相同位置是否都有这个字符
        for (int j = 1; j < strs.Length; j++)
        {
            if (i >= strs[j].Length || strs[j][i] != currentChar)
                return strs[0].Substring(0, i);
        }
    }
    
    return strs[0]; // 第一个字符串就是公共前缀
}

分治法将问题分解为子问题:

public static string LongestCommonPrefixDivide(string[] strs)
{
    if (strs == null || strs.Length == 0) return "";
    return Divide(strs, 0, strs.Length - 1);
}

private static string Divide(string[] strs, int left, int right)
{
    if (left == right) return strs[left];
    
    int mid = (left + right) / 2;
    string leftPrefix = Divide(strs, left, mid);
    string rightPrefix = Divide(strs, mid + 1, right);
    
    return CommonPrefix(leftPrefix, rightPrefix);
}

private static string CommonPrefix(string str1, string str2)
{
    int minLength = Math.Min(str1.Length, str2.Length);
    for (int i = 0; i < minLength; i++)
    {
        if (str1[i] != str2[i])
            return str1.Substring(0, i);
    }
    return str1.Substring(0, minLength);
}

二分查找法可以在某些情况下更高效:

public static string LongestCommonPrefixBinary(string[] strs)
{
    if (strs == null || strs.Length == 0) return "";
    
    // 找到最短字符串的长度
    int minLen = int.MaxValue;
    foreach (string str in strs)
        minLen = Math.Min(minLen, str.Length);
    
    int low = 1;
    int high = minLen;
    
    while (low <= high)
    {
        int middle = (low + high) / 2;
        if (IsCommonPrefix(strs, middle))
            low = middle + 1;
        else
            high = middle - 1;
    }
    
    return strs[0].Substring(0, (low + high) / 2);
}

private static bool IsCommonPrefix(string[] strs, int len)
{
    string prefix = strs[0].Substring(0, len);
    for (int i = 1; i < strs.Length; i++)
    {
        if (!strs[i].StartsWith(prefix))
            return false;
    }
    return true;
}

二分查找法的时间复杂度是O(S*log(minLen)),其中S是所有字符串的总长度。当字符串很长但公共前缀很短时,这种方法比纵向扫描的O(S)可能更优。

在实际项目中,我通常从最简单的纵向扫描开始,只有在性能成为瓶颈时才考虑更复杂的算法。过早优化往往是浪费时间的根源。

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐