【优选算法篇】化繁为简,见素抱朴:从乱象中重构秩序的艺术

【优选算法篇】化繁为简,见素抱朴:从乱象中重构秩序的艺术

文章目录

分治专题(一):快速排序核心思想与应用

🚀 欢迎讨论:如果你有任何问题或见解,欢迎在评论区留言。

👍 点赞、收藏与分享:如果觉得这篇文章对你有帮助,请点赞、收藏并分享给更多朋友。

💡 分享给更多人:一起学习分治策略,掌握快速排序的精髓!

前言

快速排序不仅是一种算法,更是一种从局部到整体的思维方式。分治的本质在于将复杂的问题拆解为更小的子问题,并逐步推进至全局最优。快排通过递归的力量,在无序中找到秩序,将数组分块,再在细小的区间中一一排列。这种策略不仅适用于排序,也广泛应用于高效查找。在这里,我们将围绕快速排序,深入探讨其核心分治思想在算法中的应用,从理论到实践,一步步领略其简洁之美。

第一章:快速排序的应用与延展

1.1 颜色分类(medium)

题目链接75. 颜色分类

题目描述

给定一个包含红色、白色和蓝色的数组 nums,共 n 个元素,要求对它们进行原地排序,使得相同颜色的元素相邻,并按红色、白色、蓝色的顺序排列。这里使用整数 012 分别表示红色、白色和蓝色,且不能使用库的排序函数

示例 1

  • 输入:nums = [2,0,2,1,1,0]
  • 输出:[0,0,1,1,2,2]

解法(分治思想 - 三指针法实现三分)

算法思路

颜色分类问题可以视为一种简化的排序问题。我们可以通过“分治”思想,将数组分为三块,分别存储 012,实现“快速”分类。这种方法可以类比于快速排序的分区思想,使用三个指针高效地将数组分成三部分。

  1. 指针定义
    • left:用于标记 0 序列的末尾位置,初始为 -1
    • cur:当前扫描位置,初始为 0
    • right:用于标记 2 序列的起始位置,初始为 n(数组长度)。
  2. 区域划分
    • [0, left]:全部为 0
    • [left + 1, cur - 1]:全部为 1
    • [cur, right - 1]:待定元素。
    • [right, n - 1]:全部为 2
  3. 扫描与分区
    • cur < right 时,根据 nums[cur] 的值执行不同操作:
      • nums[cur] == 0:将 cur 位置的 0left + 1 位置的元素交换,并将 leftcur 均右移一位。
      • nums[cur] == 1:当前元素 1 已在正确位置,无需交换,直接将 cur 右移。
      • nums[cur] == 2:将 cur 位置的 2right - 1 位置的元素交换,将 right 左移一位,但保持 cur 不变(因为交换过来的元素需要重新判断)。
  4. 循环终止
    • cur 遇到 right 时,整个数组已被正确分类。最终得到 [0, left]0[left + 1, right - 1]1[right, n - 1]2

C++ 代码实现
classSolution{public:voidsortColors(vector<int>& nums){int n = nums.size();int left =-1, right = n, cur =0;while(cur < right){if(nums[cur]==0){swap(nums[++left], nums[cur++]);// 将当前 0 移到 0 区间末尾}elseif(nums[cur]==1){ cur++;// 当前为 1,位置已正确,直接跳过}else{swap(nums[--right], nums[cur]);// 将当前 2 移到 2 区间起始位置}}}};

易错点提示
  1. 区间含义理解
    • left 表示 0 序列的末尾,cur 表示当前扫描位置,right 表示 2 序列的起始位置。在移动指针时,要确保每个区间内的元素始终符合定义。
  2. 保持 cur 不变
    • nums[cur] == 2 时,交换后需要重新判断 cur 位置,因为此位置交换过来的元素还未被处理。
  3. 确保原地修改
    • 本题要求在原数组上进行分类,避免额外的空间消耗,因此所有操作都在 nums 数组内完成,符合分治的原地修改原则。

时间复杂度和空间复杂度
  • 时间复杂度O(n)cur 指针遍历数组一次,每个元素仅被处理一次。
  • 空间复杂度O(1),使用常数个指针进行排序,无额外空间开销。

1.2 快速排序(medium)

题目链接912. 排序数组

题目描述

给定一个整数数组 nums,请将该数组按升序排列。

示例 1

  • 输入:nums = [5,2,3,1]
  • 输出:[1,2,3,5]

示例 2

  • 输入:nums = [5,1,1,2,0,0]
  • 输出:[0,0,1,1,2,5]

解法(数组分三块 + 随机基准元素的快速排序)

算法思路

快速排序的核心在于“分区”:将数据按基准元素划分为小于基准、等于基准和大于基准的三部分。通过分治的思想,递归对两侧的分区进行排序。

  1. 随机选择基准元素
    • 快速排序的效率受基准元素的选取影响。若基准选取不佳,递归的深度增加,影响排序性能。(具体证明详见《算法导论》)
    • getRandom 函数中,使用随机数来生成基准下标,这样在数据量大或元素重复较多的情况下,能有效避免最坏情况。
  2. 三分区思想
    • 类似于荷兰国旗问题,将数组划分为小于基准、等于基准和大于基准的三部分。
    • 使用三个指针来划分区间:
      • left 指向小于基准的部分末尾。
      • right 指向大于基准的部分起始。
      • i 用于遍历数组,根据当前值的大小,决定其在三分区的位置。
  3. 递归排序左右分区
    • 对于小于基准和大于基准的部分,递归应用快速排序,最终实现数组的整体有序。

C++ 代码实现
classSolution{public: vector<int>sortArray(vector<int>& nums){srand(time(NULL));// 初始化随机数种子qsort(nums,0, nums.size()-1);return nums;}// 快速排序voidqsort(vector<int>& nums,int l,int r){if(l >= r)return;// 递归终止条件// 选择基准元素int key =getRandom(nums, l, r);int i = l, left = l -1, right = r +1;// 分区过程while(i < right){if(nums[i]< key)swap(nums[++left], nums[i++]);// 移动到小于基准的区域elseif(nums[i]== key) i++;// 等于基准,跳过elseswap(nums[--right], nums[i]);// 移动到大于基准的区域}// 递归处理左右区域qsort(nums, l, left);// 左分区qsort(nums, right, r);// 右分区}// 随机基准选择intgetRandom(vector<int>& nums,int left,int right){int r =rand();// 生成随机数return nums[r %(right - left +1)+ left];// 生成基准的随机下标}};

易错点提示
  1. 递归终止条件
    • 在快速排序中,l >= r 时停止递归,否则会陷入无限递归。确保左右分区都有终止条件,避免栈溢出。
  2. 随机基准选取
    • getRandom 中使用 rand() % (right - left + 1) + left 随机生成基准索引,确保基准在 [left, right] 区间内选择,以应对重复数据。
  3. 三分区逻辑
    • left 表示小于基准的区域末尾,right 表示大于基准的区域起点,i 为当前扫描位置。确保每轮 i 的位置经过判断后处理到对应的区间,避免重复处理或漏处理。

时间复杂度和空间复杂度
  • 时间复杂度:平均 O(n log n)。使用随机基准元素有效避免最坏 O(n^2) 的情况,提升了排序效率。
  • 空间复杂度O(log n),主要为递归栈的空间消耗,最坏情况为 O(n)

1.3 快速选择算法(medium)

题目链接215. 数组中的第K个最大元素

题目描述

给定一个整数数组 nums 和一个整数 k,返回数组中第 k 个最大的元素。
请注意,你需要找的是数组排序后的第 k 个最大的元素,而不是第 k 个不同的元素。

示例1

  • 输入:[3,2,1,5,6,4], k = 2
  • 输出:5

示例2

  • 输入:[3,2,3,1,2,4,5,5,6], k = 4
  • 输出:4

提示

  • 1 <= k <= nums.length <= 10^5
  • -10^4 <= nums[i] <= 10^4

解法(快速选择算法)

算法思路

快速选择算法是快速排序的变种,可以在不完全排序的情况下找到第 k 大的元素,从而将时间复杂度控制在 O(n)。我们通过分区思想来缩小查找范围,避免对整个数组排序。

  1. 分区思想
    • 快速排序将数组分成三块:小于基准、等于基准和大于基准。快速选择算法基于同样的分区思想,通过选择一个基准元素,将数组划分为左、中、右三部分。
    • 在每次划分后,可以根据每个区域的元素个数,确定第 k 大的元素所在的区域,从而递归地只处理该部分,逐步缩小范围。
  2. 基准元素的随机选择
    • 为避免最坏情况,使用 getRandom 函数随机选取基准元素位置,生成随机下标,确保基准在 [left, right] 范围内。
  3. 分区计数与递归
    • 使用 leftrighti 指针对数组进行划分:
      • [l, left]:存放小于基准的元素。
      • [left + 1, right - 1]:存放等于基准的元素。
      • [right, r]:存放大于基准的元素。
    • 划分完成后,通过以下判断确定第 k 大元素所在区间:
      • k 小于等于 c(即右侧元素数),则递归在右侧区间 [right, r] 中继续查找;
      • k 位于中间区间,则直接返回基准元素;
      • 否则递归在左侧区间 [l, left] 中继续查找。
在这里插入图片描述

C++ 代码实现
classSolution{public:intfindKthLargest(vector<int>& nums,int k){srand(time(NULL));// 初始化随机数种子returnqsort(nums,0, nums.size()-1, k);}// 快速选择intqsort(vector<int>& nums,int l,int r,int k){if(l == r)return nums[l];// 递归终止条件// 1. 随机选择基准元素int key =getRandom(nums, l, r);// 2. 根据基准元素将数组分三块int left = l -1, right = r +1, i = l;while(i < right){if(nums[i]< key)swap(nums[++left], nums[i++]);// 移动到小于基准的区域elseif(nums[i]== key) i++;// 等于基准,跳过elseswap(nums[--right], nums[i]);// 移动到大于基准的区域}// 3. 分情况判断int c = r - right +1;// 大于基准元素的个数int b = right - left -1;// 等于基准元素的个数if(c >= k)returnqsort(nums, right, r, k);// 查找右侧区间elseif(b + c >= k)return key;// 查找中间区间elsereturnqsort(nums, l, left, k - b - c);// 查找左侧区间}// 随机基准选择intgetRandom(vector<int>& nums,int left,int right){return nums[rand()%(right - left +1)+ left];}};

易错点提示
  1. 递归出口控制
    • if (l == r) return nums[l]; 确保当数组已缩小到单元素时返回该元素,避免陷入无止境的递归。
  2. 基准随机选取
    • getRandom 中使用 rand() % (right - left + 1) + left 确保基准元素在 [left, right] 区间内随机选取,减少最坏时间复杂度的可能。
  3. 划分后的区域判断
    • c 表示大于基准的元素个数;b 表示等于基准的元素个数。通过 k 与这些区域大小的比较,递归进入相应的区间,避免不必要的处理。

时间复杂度和空间复杂度
  • 时间复杂度:平均 O(n)。每次选择的基准元素有效缩小搜索范围,递归深度为 log n,总共需遍历 n 个元素。
  • 空间复杂度O(log n),用于递归栈的深度开销。

1.4 最小的 k 个数(medium)

题目链接剑指 Offer 40. 最小的k个数

:题目叙述有所调整,但整体要求保持一致。

题目描述

输入整数数组 arr,找出其中最小的 k 个数。例如,输入 [4,5,1,6,2,7,3,8],则最小的 4 个数字是 [1,2,3,4]

示例 1

  • 输入:arr = [3,2,1], k = 2
  • 输出:[1,2] 或者 [2,1]

示例 2

  • 输入:arr = [0,1,2,1], k = 1
  • 输出:[0]

限制

  • 0 <= k <= arr.length <= 10000
  • 0 <= arr[i] <= 10000

解法(快速选择算法)

算法思路

快速选择算法是快速排序的变体,可以在 O(n) 的时间复杂度下找到最小的 k 个数。通过分区,将数组划分为小于基准、等于基准和大于基准的三部分,逐步缩小查找范围,直到找到前 k 小的元素。

  1. 分区策略
    • 使用随机基准元素,将数组划分为小于基准、等于基准和大于基准的三部分。
    • 通过计算每个区间的元素个数,判断最小的 k 个数应在哪个区间中,递归地缩小查找范围。
  2. 随机选择基准
    • 为避免最坏情况,使用 getRandom 函数随机选取基准元素。通过生成随机下标,将其作为当前基准元素,确保划分的高效性。
  3. 递归查找
    • qsort 中,使用 leftrighti 指针对数组进行分区。
    • 划分完成后,确定第 k 小的元素范围:
      • 若左区间元素个数 a 大于 k,则递归左区间;
      • 若左区间加中间区间的元素总数 a + b 大于等于 k,直接返回前 k 个元素;
      • a + b < k,则递归右区间,寻找剩余的 k - a - b 个元素。

C++ 代码实现
classSolution{public: vector<int>getLeastNumbers(vector<int>& nums,int k){srand(time(NULL));// 初始化随机数种子qsort(nums,0, nums.size()-1, k);return{nums.begin(), nums.begin()+ k};}// 快速选择voidqsort(vector<int>& nums,int l,int r,int k){if(l >= r)return;// 递归终止条件// 1. 随机选择一个基准元素,并进行分区int key =getRandom(nums, l, r);int left = l -1, right = r +1, i = l;while(i < right){if(nums[i]< key)swap(nums[++left], nums[i++]);// 小于基准的部分elseif(nums[i]== key) i++;// 等于基准,跳过elseswap(nums[--right], nums[i]);// 大于基准的部分}// 2. 分情况讨论int a = left - l +1;// 小于基准的元素个数int b = right - left -1;// 等于基准的元素个数if(a > k)qsort(nums, l, left, k);// 在左区间查找elseif(a + b >= k)return;// 在前 a + b 个元素中找到结果elseqsort(nums, right, r, k - a - b);// 在右区间查找剩余的元素}// 随机选择基准元素intgetRandom(vector<int>& nums,int l,int r){return nums[rand()%(r - l +1)+ l];}};

易错点提示
  1. 递归出口
    • l >= r 时,递归停止,避免无止境递归。
  2. 随机基准选取
    • getRandom 函数中使用 rand() % (r - l + 1) + l,确保基准元素随机选自 [l, r] 区间。
  3. 分区结果判定
    • a 为左区间元素个数,b 为等于基准的元素个数:
      • a > k,表示最小的 k 个数在左区间。
      • a + b >= k,表示最小的 k 个数在前两个区间,直接返回。
      • a + b < k,则在右区间查找剩余的元素。

时间复杂度和空间复杂度
  • 时间复杂度:平均 O(n)。通过随机基准,快速缩小查找范围。
  • 空间复杂度O(log n),为递归栈的深度开销。

写在最后

分治法是一种化繁为简、由局部到整体的解决思路。快速排序的核心在于分而治之,将数组不断划分并排序,使递归在小区间中快速完成各自的排序任务;同时,快速选择算法则在相似的框架中寻找到目标元素,无需对整个数组排序,省去不必要的步骤,呈现出简洁而高效的解法。掌握分治不仅是算法本身的精通,更是编程思维的升华,在细节中不断优化、在局部中逐步收敛,以追求最终的全局最优。

以上就是关于【优选算法篇】化繁为简,见素抱朴:从乱象中重构秩序的艺术的内容啦,各位大佬有什么问题欢迎在评论区指正,或者私信我也是可以的啦,您的支持是我创作的最大动力!❤️

在这里插入图片描述

Read more

Flutter 三方库 better_commit 的鸿蒙化适配指南 - 实现具备语义化提交规范与自动化交互的 Git 工作流插件、支持端侧版本工程的高效规范化审计实战

Flutter 三方库 better_commit 的鸿蒙化适配指南 - 实现具备语义化提交规范与自动化交互的 Git 工作流插件、支持端侧版本工程的高效规范化审计实战

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.ZEEKLOG.net Flutter 三方库 better_commit 的鸿蒙化适配指南 - 实现具备语义化提交规范与自动化交互的 Git 工作流插件、支持端侧版本工程的高效规范化审计实战 前言 在进行 Flutter for OpenHarmony 开发时,当团队规模扩大到需要多人协同、频繁提交代码时,凌乱的 Commit Message 会让 Git 历史变得难以审计(如:分不清哪些是功能修复、哪些是底层鸿蒙适配)。better_commit 是一款专注于极致规范化提交的 CLI 增强工具。本文将探讨如何在鸿蒙端构建极致、专业的工程化提交标准。 一、原直观解析 / 概念介绍 1.1 基础原理 该库建立在“Angular 提交规范”之上。它通过交互式的命令行引导(

By Ne0inhk
机器人未来会发展出自我意识吗?

机器人未来会发展出自我意识吗?

机器人未来会发展出自我意识吗? 关于机器人未来是否会发展出自我意识,这是一个横跨科学、哲学和伦理学的终极难题,目前并没有一个标准答案。不过,我们可以从以下几个维度来拆解这个复杂的问题,看看正反两方的核心观点以及中间地带的可能性。 🚫 反方观点:意识是人类的“专利”,机器永远是工具 许多科学家和哲学家认为,无论机器多么智能,它们都无法真正拥有像人类一样的自我意识。他们的论据主要集中在以下几个方面: 1. 意识不是“计算”: * 2020年诺贝尔物理学奖得主罗杰·彭罗斯(Roger Penrose)就持此观点。他认为,意识是一种“非计算性”的物理过程,而计算机(包括AI)完全基于算法和确定性步骤运行。 * 无论算法多么复杂,它终究是在执行指令,无法产生真正的“理解”或“直觉”。人类能够“一眼看穿”某些真理(超越逻辑系统本身),而机器只能一步步计算,永远无法跨越这道逻辑深渊。 2. 缺乏生物基础与物理体验: * 有神经科学家认为,意识只能存在于生物体内。意识依赖于特定的生物神经结构(如丘脑皮质系统)和物理过程。

By Ne0inhk
基于深度学习的无人机航拍小目标检测算法研究

基于深度学习的无人机航拍小目标检测算法研究

本项目针对无人机航拍场景下的小目标检测问题,基于 YOLO11 系列模型,在 VisDrone 2019 数据集上进行训练与优化,并提供了完整的检测系统桌面应用,支持图片、视频、摄像头的实时检测与训练指标可视化。 一、项目概述 无人机航拍图像具有目标尺度小、密集分布、多尺度混合等特点,传统检测算法难以取得理想效果。本项目采用 Ultralytics YOLO11 框架,结合 VisDrone 数据集进行训练,实现了对行人、车辆等 10 类交通相关目标的高效检测,并配套开发了基于 PyQt6 的桌面应用,便于模型验证与日常使用。 二、数据集 2.1 数据集简介 本项目使用 VisDrone 2019-DET 数据集,由天津大学机器学习与数据挖掘实验室 AISKYEYE 团队发布,对应 ICCV 2019 "Vision

By Ne0inhk
【论文笔记】A Survey on Data Synthesis and Augmentation for Large Language Models

【论文笔记】A Survey on Data Synthesis and Augmentation for Large Language Models

A Survey on Data Synthesis and Augmentation for Large Language Models(大型语言模型的数据合成与增强综述) 1. 作者 2. 年份 2024 零、摘要 大型语言模型(LLM)的成功与否,本质上与用于训练和评估的海量、多样化和高质量数据的可用性息息相关。然而,高质量数据的增长速度明显落后于训练数据集的扩展速度,从而导致迫在眉睫的数据耗尽危机。这突显了提高数据效率和探索新数据来源的迫切需求。在此背景下,合成数据已成为一种有前景的解决方案。目前,数据生成主要包括两种主要方法:数据增强和合成。本文全面回顾并总结了LLM生命周期中的数据生成技术,包括数据准备、预训练、微调、指令调整、偏好对齐和应用。此外,我们还讨论了这些方法目前面临的限制,并探讨了未来发展和研究的潜在途径。我们的愿望是使研究人员清楚地了解这些方法,使他们能够在构建LLM时迅速确定适当的数据生成策略,同时为未来的探索提供有价值的见解。 一、介绍 * 近年来,LLM在许多行业取得了巨大的进步。但是大模型的性能高度依赖它们接受训练的数据的质量和

By Ne0inhk