mirror of
https://github.com/zhwei820/learn.lianglianglee.com.git
synced 2025-09-25 04:36:41 +08:00
355 lines
23 KiB
HTML
355 lines
23 KiB
HTML
<!DOCTYPE html>
|
||
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
|
||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||
<head>
|
||
<head>
|
||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
|
||
<link rel="icon" href="/static/favicon.png">
|
||
<title>12 分治:如何利用分治法完成数据查找?.md.html</title>
|
||
<!-- Spectre.css framework -->
|
||
<link rel="stylesheet" href="/static/index.css">
|
||
<!-- theme css & js -->
|
||
<meta name="generator" content="Hexo 4.2.0">
|
||
</head>
|
||
<body>
|
||
<div class="book-container">
|
||
<div class="book-sidebar">
|
||
<div class="book-brand">
|
||
<a href="/">
|
||
<img src="/static/favicon.png">
|
||
<span>技术文章摘抄</span>
|
||
</a>
|
||
</div>
|
||
<div class="book-menu uncollapsible">
|
||
<ul class="uncollapsible">
|
||
<li><a href="/" class="current-tab">首页</a></li>
|
||
</ul>
|
||
<ul class="uncollapsible">
|
||
<li><a href="../">上一级</a></li>
|
||
</ul>
|
||
<ul class="uncollapsible">
|
||
<li>
|
||
|
||
<a href="/专栏/重学数据结构与算法-完/00 数据结构与算法,应该这样学!.md.html">00 数据结构与算法,应该这样学!</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/重学数据结构与算法-完/01 复杂度:如何衡量程序运行的效率?.md.html">01 复杂度:如何衡量程序运行的效率?</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/重学数据结构与算法-完/02 数据结构:将“昂贵”的时间复杂度转换成“廉价”的空间复杂度.md.html">02 数据结构:将“昂贵”的时间复杂度转换成“廉价”的空间复杂度</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/重学数据结构与算法-完/03 增删查:掌握数据处理的基本操作,以不变应万变.md.html">03 增删查:掌握数据处理的基本操作,以不变应万变</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/重学数据结构与算法-完/04 如何完成线性表结构下的增删查?.md.html">04 如何完成线性表结构下的增删查?</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/重学数据结构与算法-完/05 栈:后进先出的线性表,如何实现增删查?.md.html">05 栈:后进先出的线性表,如何实现增删查?</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/重学数据结构与算法-完/06 队列:先进先出的线性表,如何实现增删查?.md.html">06 队列:先进先出的线性表,如何实现增删查?</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/重学数据结构与算法-完/07 数组:如何实现基于索引的查找?.md.html">07 数组:如何实现基于索引的查找?</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/重学数据结构与算法-完/08 字符串:如何正确回答面试中高频考察的字符串匹配算法?.md.html">08 字符串:如何正确回答面试中高频考察的字符串匹配算法?</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/重学数据结构与算法-完/09 树和二叉树:分支关系与层次结构下,如何有效实现增删查?.md.html">09 树和二叉树:分支关系与层次结构下,如何有效实现增删查?</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/重学数据结构与算法-完/10 哈希表:如何利用好高效率查找的“利器”?.md.html">10 哈希表:如何利用好高效率查找的“利器”?</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/重学数据结构与算法-完/11 递归:如何利用递归求解汉诺塔问题?.md.html">11 递归:如何利用递归求解汉诺塔问题?</a>
|
||
</li>
|
||
<li>
|
||
<a class="current-tab" href="/专栏/重学数据结构与算法-完/12 分治:如何利用分治法完成数据查找?.md.html">12 分治:如何利用分治法完成数据查找?</a>
|
||
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/重学数据结构与算法-完/13 排序:经典排序算法原理解析与优劣对比.md.html">13 排序:经典排序算法原理解析与优劣对比</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/重学数据结构与算法-完/14 动态规划:如何通过最优子结构,完成复杂问题求解?.md.html">14 动态规划:如何通过最优子结构,完成复杂问题求解?</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/重学数据结构与算法-完/15 定位问题才能更好地解决问题:开发前的复杂度分析与技术选型.md.html">15 定位问题才能更好地解决问题:开发前的复杂度分析与技术选型</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/重学数据结构与算法-完/16 真题案例(一):算法思维训练.md.html">16 真题案例(一):算法思维训练</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/重学数据结构与算法-完/17 真题案例(二):数据结构训练.md.html">17 真题案例(二):数据结构训练</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/重学数据结构与算法-完/18 真题案例(三):力扣真题训练.md.html">18 真题案例(三):力扣真题训练</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/重学数据结构与算法-完/19 真题案例(四):大厂真题实战演练.md.html">19 真题案例(四):大厂真题实战演练</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/重学数据结构与算法-完/20 代码之外,技术面试中你应该具备哪些软素质?.md.html">20 代码之外,技术面试中你应该具备哪些软素质?</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/重学数据结构与算法-完/21 面试中如何建立全局观,快速完成优质的手写代码?.md.html">21 面试中如何建立全局观,快速完成优质的手写代码?</a>
|
||
</li>
|
||
<li>
|
||
|
||
<a href="/专栏/重学数据结构与算法-完/加餐 课后练习题详解.md.html">加餐 课后练习题详解</a>
|
||
</li>
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
|
||
<div class="sidebar-toggle-inner"></div>
|
||
</div>
|
||
<script>
|
||
function add_inner() {
|
||
let inner = document.querySelector('.sidebar-toggle-inner')
|
||
inner.classList.add('show')
|
||
}
|
||
function remove_inner() {
|
||
let inner = document.querySelector('.sidebar-toggle-inner')
|
||
inner.classList.remove('show')
|
||
}
|
||
function sidebar_toggle() {
|
||
let sidebar_toggle = document.querySelector('.sidebar-toggle')
|
||
let sidebar = document.querySelector('.book-sidebar')
|
||
let content = document.querySelector('.off-canvas-content')
|
||
if (sidebar_toggle.classList.contains('extend')) { // show
|
||
sidebar_toggle.classList.remove('extend')
|
||
sidebar.classList.remove('hide')
|
||
content.classList.remove('extend')
|
||
} else { // hide
|
||
sidebar_toggle.classList.add('extend')
|
||
sidebar.classList.add('hide')
|
||
content.classList.add('extend')
|
||
}
|
||
}
|
||
function open_sidebar() {
|
||
let sidebar = document.querySelector('.book-sidebar')
|
||
let overlay = document.querySelector('.off-canvas-overlay')
|
||
sidebar.classList.add('show')
|
||
overlay.classList.add('show')
|
||
}
|
||
function hide_canvas() {
|
||
let sidebar = document.querySelector('.book-sidebar')
|
||
let overlay = document.querySelector('.off-canvas-overlay')
|
||
sidebar.classList.remove('show')
|
||
overlay.classList.remove('show')
|
||
}
|
||
</script>
|
||
<div class="off-canvas-content">
|
||
<div class="columns">
|
||
<div class="column col-12 col-lg-12">
|
||
<div class="book-navbar">
|
||
<!-- For Responsive Layout -->
|
||
<header class="navbar">
|
||
<section class="navbar-section">
|
||
<a onclick="open_sidebar()">
|
||
<i class="icon icon-menu"></i>
|
||
</a>
|
||
</section>
|
||
</header>
|
||
</div>
|
||
<div class="book-content" style="max-width: 960px; margin: 0 auto;
|
||
overflow-x: auto;
|
||
overflow-y: hidden;">
|
||
<div class="book-post">
|
||
<p id="tip" align="center"></p>
|
||
<div><h1>12 分治:如何利用分治法完成数据查找?</h1>
|
||
<p>前面课时中,我们学习了递归的思想,它是一种函数自我调用缩小问题规模的方法。这一课时我们继续学习另一种算法思维,分治法。</p>
|
||
<p>从定性的角度来看,分治法的核心思想就是“分而治之”。利用分而治之的思想,就可以把一个大规模、高难度的问题,分解为若干个小规模、低难度的小问题。随后,开发者将面对多个简单的问题,并很快地找到答案各个击破。在把这些简单问题解决好之后,我们通过把这些小问题的答案合并,就得到了原问题的答案。</p>
|
||
<p>分治法应用很广泛,很多高效率的算法都是以分治法作为其基础思想,例如排序算法中的快速排序和归并排序。</p>
|
||
<h3>分治法是什么?</h3>
|
||
<p>计算机求解问题所需的计算时间,与其涉及的数据规模强相关。简而言之,问题所涉及的数据规模越小,它所需的计算时间也越少;反之亦然。</p>
|
||
<p>我们来看一个例子:<strong>在一个包含 n 个元素的无序数组中,要求按照从小到大的顺序打印其 n 个元素。</strong></p>
|
||
<p>假设我们采用 n 个元素之间的两两比较的计算方法,去得到从小到大的序列。分析如下:</p>
|
||
<p>当数据量 n = 1 时,不需任何计算,直接打印即可;</p>
|
||
<p>当数据量 n = 2 时 ,那需要做 1 次比较即可达成目标;</p>
|
||
<p>当数据量 n = 3 时,要对这 3 个元素进行两两比较,共计 3 次比较;</p>
|
||
<p>而当数据量 n = 10 时,问题就不那么容易处理了,我们需要 45 次比较(计算方式是 0.5*n(n-1) )。</p>
|
||
<p>因此,要想通过上述方法直接解决一个规模较大的问题,其实是相当困难的。</p>
|
||
<p>基于此,<strong>分治法的核心思想就是分而治之</strong>。具体来说,它先将一个难以直接解决的大问题,分割成一些可以直接解决的小问题。如果分割后的问题仍然无法直接解决,那么就继续递归地分割,直到每个小问题都可解。</p>
|
||
<p>通常而言,这些子问题具备互相独立、形式相同的特点。这样,我们就可以采用同一种解法,递归地去解决这些子问题。最后,再将每个子问题的解合并,就得到了原问题的解。</p>
|
||
<h4>分治法的价值</h4>
|
||
<p>关于分治法,很多同学都有这样一个误区。那就是,当你的计算机性能还不错的时候,采用分治法相对于全局遍历一遍没有什么差别。</p>
|
||
<p>例如下面这个问题,<strong>在 1000 个有序数字构成的数组 a 中,判断某个数字 c 是否出现过。</strong></p>
|
||
<p><strong>第一种方法,全局遍历。</strong> 复杂度 O(n)。采用 for 循环,对 1000 个数字全部判断一遍。</p>
|
||
<p><strong>第二种方法,采用二分查找。</strong> 复杂度 O(logn)。递归地判断 c 与 a 的中位数的大小关系,并不断缩小范围。</p>
|
||
<p>这两种方法,对时间的消耗几乎一样。那分治法的价值又是什么呢?</p>
|
||
<p>其实,在小数据规模上,分治法没有什么特殊价值。无非就是让代码显得更牛一些。只有在大数据集上,分治法的价值才能显现出来。</p>
|
||
<p>下面我们通过一个经典的案例带你感受分治法的价值。</p>
|
||
<p><strong>假如有一张厚度为 1 毫米且足够柔软的纸,问将它对折多少次之后,厚度能达到地球到月球的距离?</strong></p>
|
||
<p>这个问题看起来很异想天开。根据百度百科,地月平均距离是 384,403.9 千米,大约 39 万千米。粗看怎么也需要对折 1 万次吧?但实际上,根据计算,我们只需要对折 39 次就够了。计算的过程是 2^39 = 549,755,813,888 = 55 万千米 > 39 万千米。那么,这个例子意味着什么呢?</p>
|
||
<p>我们回到前面讲到的在数组 a 中查找数字 c 的例子,如果数组 a 的大小拓展到 549,755,813,888 这个量级上,使用第二种的二分查找方法,仅仅需要 39 次判断,就能找到最终结果。相比暴力搜索的方法,性能优势高的不是一星半点!这也证明了,<strong>复杂度为 O(logn) 相比复杂度为 O(n) 的算法,在大数据集合中性能有着爆发式的提高。</strong></p>
|
||
<h3>分治法的使用方法</h3>
|
||
<p>前面我们讲到分治法的核心思想是“分而治之”,当你需要采用分治法时,<strong>一般原问题都需要具备以下几个特征:</strong></p>
|
||
<ol>
|
||
<li><strong>难度在降低</strong>,即原问题的解决难度,随着数据的规模的缩小而降低。这个特征绝大多数问题都是满足的。</li>
|
||
<li><strong>问题可分</strong>,原问题可以分解为若干个规模较小的同类型问题。这是应用分治法的前提。</li>
|
||
<li><strong>解可合并</strong>,利用所有子问题的解,可合并出原问题的解。这个特征很关键,能否利用分治法完全取决于这个特征。</li>
|
||
<li><strong>相互独立</strong>,各个子问题之间相互独立,某个子问题的求解不会影响到另一个子问题。如果子问题之间不独立,则分治法需要重复地解决公共的子问题,造成效率低下的结果。</li>
|
||
</ol>
|
||
<p>根据前面我们对分治法的分析,你一定能迅速联想到递归。分治法需要递归地分解问题,再去解决问题。因此,<strong>分治法在每轮递归上,都包含了分解问题、解决问题和合并结果这 3 个步骤。</strong></p>
|
||
<p>为了让大家对分治法有更清晰地了解,我们以二分查找为例,看一下分治法如何使用。关于分治法在排序中的使用,我们会在第 11 课时中讲到。查找问题指的是,在一个有序的数列中,判断某个待查找的数字是否出现过。二分查找,则是利用分治法去解决查找问题。通常二分查找需要一个前提,那就是输入的数列是有序的。</p>
|
||
<p><strong>二分查找的思路比较简单,步骤如下</strong>:</p>
|
||
<ol>
|
||
<li>选择一个标志 i 将集合 L 分为二个子集合,一般可以使用中位数;</li>
|
||
<li>判断标志 L(i) 是否能与要查找的值 des 相等,相等则直接返回结果;</li>
|
||
<li>如果不相等,需要判断 L(i) 与 des 的大小;</li>
|
||
<li>基于判断的结果决定下步是向左查找还是向右查找。如果向某个方向查找的空间为 0,则返回结果未查到;</li>
|
||
<li>回到步骤 1。</li>
|
||
</ol>
|
||
<p>我们对二分查找的复杂度进行分析。二分查找的最差情况是,不断查找到最后 1 个数字才完成判断。那么此时需要的最大的复杂度就是 O(logn)。</p>
|
||
<h3>分治法的案例</h3>
|
||
<p>下面我们一起来看一个例子。<strong>在数组 { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 } 中,查找 8 是否出现过。</strong></p>
|
||
<p>首先判断 8 和中位数 5 的大小关系。因为 8 更大,所以在更小的范围 6, 7, 8, 9, 10 中继续查找。此时更小的范围的中位数是 8。由于 8 等于中位数 8,所以查找到并打印查找到的 8 对应在数组中的 index 值。如下图所示。</p>
|
||
<p><img src="assets/Ciqc1F7zEOSAElX7ABXXgmxI808203.gif" alt="png" /></p>
|
||
<p>从代码实现的角度来看,我们可以采用两个索引 low 和 high,确定查找范围。最初 low 为 0,high 为数组长度减 1。在一个循环体内,判断 low 到 high 的中位数与目标变量 targetNumb 的大小关系。根据结果确定向左走(high = middle - 1)或者向右走(low = middle + 1),来调整 low 和 high 的值。直到 low 反而比 high 更大时,说明查找不到并跳出循环。我们给出代码如下:</p>
|
||
<pre><code>public static void main(String[] args) {
|
||
// 需要查找的数字
|
||
int targetNumb = 8;
|
||
// 目标有序数组
|
||
int[] arr = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
|
||
int middle = 0;
|
||
int low = 0;
|
||
int high = arr.length - 1;
|
||
int isfind = 0;
|
||
while (low <= high) {
|
||
middle = (high + low) / 2;
|
||
if (arr[middle] == targetNumb) {
|
||
System.out.println(targetNumb + " 在数组中,下标值为: " + middle);
|
||
isfind = 1;
|
||
break;
|
||
} else if (arr[middle] > targetNumb) {
|
||
// 说明该数在low~middle之间
|
||
high = middle - 1;
|
||
} else {
|
||
// 说明该数在middle~high之间
|
||
low = middle + 1;
|
||
}
|
||
}
|
||
if (isfind == 0) {
|
||
System.out.println("数组不含 " + targetNumb);
|
||
}
|
||
}
|
||
</code></pre>
|
||
<p>我们基于这个例子,可以对它进行一些经验和规律的总结,这些经验会辅助大家在面试时找到解题思路。</p>
|
||
<ol>
|
||
<li><strong>二分查找的时间复杂度是 O(logn),这也是分治法普遍具备的特性。当你面对某个代码题,而且约束了时间复杂度是 O(logn) 或者是 O(nlogn) 时,可以想一下分治法是否可行。</strong></li>
|
||
<li><strong>二分查找的循环次数并不确定。一般是达到某个条件就跳出循环。因此,编码的时候,多数会采用 while 循环加 break 跳出的代码结构。</strong></li>
|
||
<li><strong>二分查找处理的原问题必须是有序的。因此,当你在一个有序数据环境中处理问题时,可以考虑分治法。相反,如果原问题中的数据并不是有序的,则使用分治法的可能性就会很低了。</strong></li>
|
||
</ol>
|
||
<p>以上 3 点经验和规律的总结,可以帮助你快速找到解决方案,做好技术选型。在实际工作和参加面试时,都是非常重要的经验。</p>
|
||
<h3>练习题</h3>
|
||
<p>最后,我们给出一个进阶的问题,供大家练习。题目如下:</p>
|
||
<p><strong>在一个有序数组中,查找出第一个大于 9 的数字,假设一定存在。例如,arr = { -1, 3, 3, 7, 10, 14, 14 }; 则返回 10。</strong></p>
|
||
<p>在这里提醒一下,带查找的目标数字具备这样的性质:</p>
|
||
<p>第一,它比 9 大;</p>
|
||
<p>第二,它前面的数字(除非它是第一个数字),比 9 小。</p>
|
||
<p>因此,当我们作出向左走或向右走的决策时,必须满足这两个条件。</p>
|
||
<pre><code>public static void main(String[] args) {
|
||
int targetNumb = 9;
|
||
// 目标有序数组
|
||
int[] arr = { -1, 3, 3, 7, 10, 14, 14 };
|
||
int middle = 0;
|
||
int low = 0;
|
||
int high = arr.length - 1;
|
||
while (low <= high) {
|
||
middle = (high + low) / 2;
|
||
if (arr[middle] > targetNumb && (middle == 0 || arr[middle - 1] <= targetNumb)) {
|
||
System.out.println("第一个比 " + targetNumb + " 大的数字是 " + arr[middle]);
|
||
break;
|
||
} else if (arr[middle] > targetNumb) {
|
||
// 说明该数在low~middle之间
|
||
high = middle - 1;
|
||
} else {
|
||
// 说明该数在middle~high之间
|
||
low = middle + 1;
|
||
}
|
||
}
|
||
}
|
||
</code></pre>
|
||
<h3>总结</h3>
|
||
<p>分治法经常会用在海量数据处理中。这也是它显著区别于遍历查找方法的优势。<strong>在面对陌生问题时,需要注意原问题的数据是否有序,预期的时间复杂度是否带有 logn 项,是否可以通过小问题的答案合并出原问题的答案。如果这些先决条件都满足,你就应该第一时间想到分治法。</strong></p>
|
||
</div>
|
||
</div>
|
||
<div>
|
||
<div style="float: left">
|
||
<a href="/专栏/重学数据结构与算法-完/11 递归:如何利用递归求解汉诺塔问题?.md.html">上一页</a>
|
||
</div>
|
||
<div style="float: right">
|
||
<a href="/专栏/重学数据结构与算法-完/13 排序:经典排序算法原理解析与优劣对比.md.html">下一页</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
|
||
</div>
|
||
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"70997dd71b313cfa","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
|
||
</body>
|
||
<!-- Global site tag (gtag.js) - Google Analytics -->
|
||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
|
||
<script>
|
||
window.dataLayer = window.dataLayer || [];
|
||
function gtag() {
|
||
dataLayer.push(arguments);
|
||
}
|
||
gtag('js', new Date());
|
||
gtag('config', 'G-NPSEEVD756');
|
||
var path = window.location.pathname
|
||
var cookie = getCookie("lastPath");
|
||
console.log(path)
|
||
if (path.replace("/", "") === "") {
|
||
if (cookie.replace("/", "") !== "") {
|
||
console.log(cookie)
|
||
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
|
||
}
|
||
} else {
|
||
setCookie("lastPath", path)
|
||
}
|
||
function setCookie(cname, cvalue) {
|
||
var d = new Date();
|
||
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
|
||
var expires = "expires=" + d.toGMTString();
|
||
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
|
||
}
|
||
function getCookie(cname) {
|
||
var name = cname + "=";
|
||
var ca = document.cookie.split(';');
|
||
for (var i = 0; i < ca.length; i++) {
|
||
var c = ca[i].trim();
|
||
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
|
||
}
|
||
return "";
|
||
}
|
||
</script>
|
||
</html>
|