【笔记】数组

本文是《代码随想录》和《labuladong的算法笔记》实践系列的第一篇,重点讲解数组相关题目,涵盖二分查找、双指针等多种算法技巧。内容包括有序数组的二分查找、原地修改数组、从后到前遍历、滑动窗口以及接雨水问题等经典数组操作,并结合LeetCode题目进行实例解析。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

本系列总计六篇文章,是 基于STL实现的笔试题常考七大基本数据结构 该文章在《代码随想录》和《labuladong的算法笔记》题目中的具体实践,每篇的布局是这样的:开头是该数据结构的总结,然后是在不同场景的应用,以及不同的算法技巧。本文是系列第一篇,介绍了数组的相关题目,重点是要掌握双指针算法、二分查找算法。

下面文章是在《代码随想录》和《labuladong的算法笔记》题目中的具体实践:
【笔记】数组
【笔记】链表
【笔记】哈希表
【笔记】字符串
【笔记】栈与队列
【笔记】二叉树

0、总结

  • 双指针,快慢双指针(用于数组覆盖),左右双指针(二分查找),滑动窗口(长度最小子数组)
  • 有序数组+无重复,必然想到 二分查找 及其变体,找target,找左边界,找右边界,需要坚持闭区间
  • 用循环不变量的模拟(螺旋数组)

1、二分查找-左右双指针

算法框架

注意,…号出现的四处位置,根据是否闭区间/左闭右开,是否返回一个索引/索引边界,而不同,下面我都坚持闭区间的写法

int binarySearch(int[] nums, int target) {
   
    int left = 0, right = 1...;

    while(2...) {
   
        int mid = left + (right - left) / 2;
        if (nums[mid] == target) {
   
            3...
        } else if (nums[mid] < target) {
   
            left = 3...
        } else if (nums[mid] > target) {
   
            right = 3...
        }
    }
    return 4...;
}

704. 二分查找 - 力扣(LeetCode)

思路:二分查找最基础版本,采用左闭右闭区间,因此注意这四处

1、high初始化指向最末尾元素

2、while中的判断是 <= ,因为当 left == right 时区间依然有意义

3、mid每次必须加减1,相等即返回

4、未找到返回-1

另外,写成 if - else if 形式,将逻辑分支判断展开,更有助于理解

【不足】若target有多个值,该解法只能返回其中一个值

class Solution {
   
public:
    int search(vector<int>& nums, int target) {
   
        int low = 0;
        int high = nums.size() - 1; // 1
        // 当left == right,区间[left, right]依然有效,所以用 <=
        while (low <= high) {
    // 2
            int mid = low + (high - low) / 2;
            if (nums[mid] > target) high = mid - 1; // 3
            if (nums[mid] < target) low = mid + 1; // 3
            if (nums[mid] == target) return mid; // 3
        }
        return -1; // 4
    }
};

35. 搜索插入位置 - 力扣(LeetCode)

思路:二分法的变体,能处理如下四种情况:

  • 目标值在数组所有元素之前
  • 目标值等于数组中某一个元素
  • 目标值插入数组中的位置
  • 目标值在数组所有元素之后
class Solution {
   
public:
    int searchInsert(vector<int>& nums, int target) {
   
        int low = 0;
        int high = nums.size() - 1;
        while (low <= high) {
   
            int mid = low + (high - low) / 2;
            if (nums[mid] > target) high = mid - 1;
            if (nums[mid] < target) low = mid + 1;
            if (nums[mid] == target) return mid;
        }
        return high + 1;
    }
};

!!!以上两种情况,只适合无重复元素+数组升序的版本

34. 在排序数组中查找元素的第一个和最后一个位置 - 力扣(LeetCode)

思路:二分查找变体,能找到target的左右边界

寻找target在数组里的左右边界,有如下三种情况:

  • 情况一:target 在数组范围的右边或者左边,例如数组{3, 4, 5},target为2或者数组{3, 4, 5},target为6,此时应该返回{-1, -1}
  • 情况二:target 在数组范围中,且数组中存在target,例如数组{3,6,7},target为6,此时应该返回{1, 1}
  • 情况三:target 在数组范围中,且数组中不存在target,例如数组{3,6,7},target为5,此时应该返回{-1, -1}

注意:寻找右边界时,nums[mid] > target时,直接减小right = mid - 1,**当 nums[mid] == target 时,不要立即返回,而是增大「搜索区间」的左边界 left,通过left = mid + 1,rightBorder = left,使得区间不断向右靠拢,达到锁定右侧边界的目的,nums[mid] < target时,left = mid + 1,因此小于和等于target的情况合并为一种。**左边界同理。

class Solution {
   
public:
    vector<int> searchRange(vector<int>& nums, int target) {
   
        int leftBorder = getLeftBorder(nums, target);
        int rightBorder = getRightBorder(nums, target);
        cout << leftBorder << endl;
        cout << rightBorder << endl;
        // 情况一,边界值从未被更新,说明 target 在数组范围的右边或者左边
        if (leftBorder == -2 || rightBorder == -2) return {
   -1, -1};
        // 情况二,边界值相差大于1,说明中间至少夹着一个target
        if (rightBorder - leftBorder > 1) return {
   leftBorder + 1, rightBorder - 1};
        // 情况三,边界值相邻,target 在数组范围中,但是数组中不存在target
        return {
   -1, -1};
    }
    // 寻找右边界,不包括target
    int getRightBorder(vector<int>& nums, int target) {
   
        int left = 0;
        int right = nums.size() - 1;
        // 记录初始值
        int rightBorder = -2;
        while (left <= right) {
   
            int mid = left + (right - left) / 2;
            if (nums[mid] > target) {
   
                right = mid - 1;
            } else if (nums[mid] <= target) {
    // 寻找右边界,找到target时先别返回,继续更新left
                left = mid + 1;
                rightBorder = left;
            }
        }
        return rightBorder;
    }
    int getLeftBorder(vector<int>& nums, int target) {
   
        int left = 0;
        int right = nums.size() - 1;
        // 记录初始值
        int leftBorder = -2;
        while (left <= right) {
   
            int mid = left + (right - left) / 2;
            if (nums[mid] < target) {
   
                left = mid + 1;
            } else if (nums[mid] >= target) {
    // 寻找左边界,用 right 来逼近
                right = mid - 1;
                leftBorder = right;
            }
        }
        return leftBorder;
    }
};

69. x 的平方根 - 力扣(LeetCode)

思路:二分查找变体,在1到x中,用二分查找,寻找x的平方根。避免平方运算溢出,应用除法代替。

由于返回类型是整数,结果只保留 整数部分 ,小数部分将被 舍去 。

class Solution {
   
public:
    int mySqrt(int x) {
   
        if (x == 0 || x == 1) return x;
        int left = 1;
        int right = x;
        while (left <= right) {
   
            int mid = left + (right - left) / 2;
            // 用除法判断,避免溢出
            int temp = x / mid;
            if (temp > mid) {
    // mid平方小于x,增大左边界
                left = mid + 1;
            } else if (temp < mid) {
    // mid平方大于x,减小右边界
                right = mid - 1;
            } else if (temp == mid) 
<think>嗯,用户想了解C#中如何流式读取文件数组。首先,我需要明确“流式读取”和“文件数组”的具体含义。可能用户指的是以流的方式逐步读取文件中的字节数组,或者处理包含多个文件的数组? 假设是前者,即逐块读取文件的字节数组,避免一次性加载整个文件到内存。这在处理大文件时很重要,可以节省内存资源。常见的流式读取类有FileStream,BufferedStream等。 首先,我应该回忆FileStream的基本用法。用File.OpenRead打开文件,然后循环读取到字节数组中。需要注意每次读取的字节数,以及处理读取的字节数可能小于请求的情况。 可能需要给出示例代码,比如使用FileStream.Read方法,在循环中读取数据块,直到返回0。同时,要强调使用using语句来正确释放资源,避免文件句柄泄漏。 另外,用户提到的“数组”可能是指将文件内容读取到字节数组中。这种情况下,流式读取是分块读取,逐步填充数组,或者处理每个数据块。需要区分一次性读取整个文件到字节数组和分块读取的区别。如果文件很大,分块处理更有效率。 可能还需要提到其他相关类,比如BinaryReader,它提供了更方便的方法读取特定数据类型,但底层也是基于流的。 用户可能的应用场景是什么?处理日志文件、上传大文件、实时处理数据流?不同的场景可能需要不同的优化,比如使用缓冲流提高性能,或者异步读取避免阻塞。 另外,异常处理也是需要注意的地方,比如文件不存在、权限问题等,在代码中应该加入try-catch块。 总结步骤:打开文件流,循环读取字节块,处理每个块,关闭流。同时注意资源管理和异常处理。 可能需要检查是否有常见的误区,比如忘记检查Read方法的返回值,导致处理多余的数据。或者在读取文本文件时,使用StreamReader更适合,按行读取,但用户提到的是数组,所以可能处理的是二进制文件。 最后,确保示例代码简洁,注释清晰,说明每个步骤的作用,并指出可能的变体和注意事项,比如缓冲区大小的选择,异步读取的方法等。</think>在 C# 中流式读取文件数组(即分块读取文件内容到字节数组)可通过以下步骤实现: ```csharp using System; using System.IO; class Program { static void Main() { const string filePath = "example.dat"; const int bufferSize = 4096; // 4KB 的缓冲区(可调整) // 使用 FileStream 打开文件(自动释放资源) using (FileStream fs = File.OpenRead(filePath)) { byte[] buffer = new byte[bufferSize]; int bytesRead; // 循环读取直到文件末尾 while ((bytesRead = fs.Read(buffer, 0, buffer.Length)) > 0) { // 注意:最后一次读取可能不满缓冲区 ProcessChunk(buffer, bytesRead); } } } static void ProcessChunk(byte[] chunkData, int validLength) { // 在此处理每个数据块(如:加密/解密、传输、解析等) Console.WriteLine($"已处理 {validLength} 字节数据"); } } ``` **关键点解析:** 1. **流式读取优势**: - 内存效率:避免一次性加载大文件(例如 1GB 文件只需 4KB 内存) - 实时性:可立即处理已读取部分,无需等待全部加载完成 2. **重要参数**: - `bufferSize`:推荐 4KB 的整数倍(与磁盘块对齐),通常 4096-81920 字节之间 - `bytesRead`:实际读取的字节数(最后一次可能小于缓冲区大小) 3. **扩展优化方案**: ```csharp // 使用 BufferedStream 提升小规模连续读取性能 using (var stream = new BufferedStream(File.OpenRead(filePath), 81920)) { // 读取逻辑同上 } // 异步读取(适合高并发场景) using (FileStream fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, true)) { byte[] buffer = new byte[4096]; int bytesRead; while ((bytesRead = await fs.ReadAsync(buffer, 0, buffer.Length)) > 0) { await ProcessAsync(buffer, bytesRead); } } ``` 4. **注意事项**: - 二进制文件:直接操作 byte[] 数组 - 文本文件:建议改用 `StreamReader` 并按行读取 - 资源释放:务必使用 `using` 语句或手动 `Dispose()` - 文件锁定:读取期间文件会被锁定(可通过 `FileShare.Read` 参数控制) **典型应用场景**: - 大文件加密/解密 - 网络文件分块传输 - 日志文件的实时解析 - 媒体文件的边读边播 是否需要进一步了解特定场景下的优化技巧?例如处理超大型文件(10GB+)时的内存映射文件方案?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值