learn.lianglianglee.com/专栏/重学数据结构与算法-完/16 真题案例(一):算法思维训练.md.html
2022-05-11 19:04:14 +08:00

845 lines
28 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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>16 真题案例(一):算法思维训练.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 数据结构与算法,应该这样学!.md.html</a>
</li>
<li>
<a href="/专栏/重学数据结构与算法-完/01 复杂度:如何衡量程序运行的效率?.md.html">01 复杂度:如何衡量程序运行的效率?.md.html</a>
</li>
<li>
<a href="/专栏/重学数据结构与算法-完/02 数据结构:将“昂贵”的时间复杂度转换成“廉价”的空间复杂度.md.html">02 数据结构:将“昂贵”的时间复杂度转换成“廉价”的空间复杂度.md.html</a>
</li>
<li>
<a href="/专栏/重学数据结构与算法-完/03 增删查:掌握数据处理的基本操作,以不变应万变.md.html">03 增删查:掌握数据处理的基本操作,以不变应万变.md.html</a>
</li>
<li>
<a href="/专栏/重学数据结构与算法-完/04 如何完成线性表结构下的增删查?.md.html">04 如何完成线性表结构下的增删查?.md.html</a>
</li>
<li>
<a href="/专栏/重学数据结构与算法-完/05 栈:后进先出的线性表,如何实现增删查?.md.html">05 栈:后进先出的线性表,如何实现增删查?.md.html</a>
</li>
<li>
<a href="/专栏/重学数据结构与算法-完/06 队列:先进先出的线性表,如何实现增删查?.md.html">06 队列:先进先出的线性表,如何实现增删查?.md.html</a>
</li>
<li>
<a href="/专栏/重学数据结构与算法-完/07 数组:如何实现基于索引的查找?.md.html">07 数组:如何实现基于索引的查找?.md.html</a>
</li>
<li>
<a href="/专栏/重学数据结构与算法-完/08 字符串:如何正确回答面试中高频考察的字符串匹配算法?.md.html">08 字符串:如何正确回答面试中高频考察的字符串匹配算法?.md.html</a>
</li>
<li>
<a href="/专栏/重学数据结构与算法-完/09 树和二叉树:分支关系与层次结构下,如何有效实现增删查?.md.html">09 树和二叉树:分支关系与层次结构下,如何有效实现增删查?.md.html</a>
</li>
<li>
<a href="/专栏/重学数据结构与算法-完/10 哈希表:如何利用好高效率查找的“利器”?.md.html">10 哈希表:如何利用好高效率查找的“利器”?.md.html</a>
</li>
<li>
<a href="/专栏/重学数据结构与算法-完/11 递归:如何利用递归求解汉诺塔问题?.md.html">11 递归:如何利用递归求解汉诺塔问题?.md.html</a>
</li>
<li>
<a href="/专栏/重学数据结构与算法-完/12 分治:如何利用分治法完成数据查找?.md.html">12 分治:如何利用分治法完成数据查找?.md.html</a>
</li>
<li>
<a href="/专栏/重学数据结构与算法-完/13 排序:经典排序算法原理解析与优劣对比.md.html">13 排序:经典排序算法原理解析与优劣对比.md.html</a>
</li>
<li>
<a href="/专栏/重学数据结构与算法-完/14 动态规划:如何通过最优子结构,完成复杂问题求解?.md.html">14 动态规划:如何通过最优子结构,完成复杂问题求解?.md.html</a>
</li>
<li>
<a href="/专栏/重学数据结构与算法-完/15 定位问题才能更好地解决问题:开发前的复杂度分析与技术选型.md.html">15 定位问题才能更好地解决问题:开发前的复杂度分析与技术选型.md.html</a>
</li>
<li>
<a class="current-tab" href="/专栏/重学数据结构与算法-完/16 真题案例(一):算法思维训练.md.html">16 真题案例(一):算法思维训练.md.html</a>
</li>
<li>
<a href="/专栏/重学数据结构与算法-完/17 真题案例(二):数据结构训练.md.html">17 真题案例(二):数据结构训练.md.html</a>
</li>
<li>
<a href="/专栏/重学数据结构与算法-完/18 真题案例(三):力扣真题训练.md.html">18 真题案例(三):力扣真题训练.md.html</a>
</li>
<li>
<a href="/专栏/重学数据结构与算法-完/19 真题案例(四):大厂真题实战演练.md.html">19 真题案例(四):大厂真题实战演练.md.html</a>
</li>
<li>
<a href="/专栏/重学数据结构与算法-完/20 代码之外,技术面试中你应该具备哪些软素质?.md.html">20 代码之外,技术面试中你应该具备哪些软素质?.md.html</a>
</li>
<li>
<a href="/专栏/重学数据结构与算法-完/21 面试中如何建立全局观,快速完成优质的手写代码?.md.html">21 面试中如何建立全局观,快速完成优质的手写代码?.md.html</a>
</li>
<li>
<a href="/专栏/重学数据结构与算法-完/加餐 课后练习题详解.md.html">加餐 课后练习题详解.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>16 真题案例(一):算法思维训练</h1>
<p>你好,欢迎进入第 16 课时的学习。在前面课时中,我们已经学习了解决代码问题的方法论。宏观上,它可以分为以下 4 个步骤:</p>
<ol>
<li><strong>复杂度分析</strong>。估算问题中复杂度的上限和下限。</li>
<li><strong>定位问题</strong>。根据问题类型,确定采用何种算法思维。</li>
<li><strong>数据操作分析</strong>。根据增、删、查和数据顺序关系去选择合适的数据结构,利用空间换取时间。</li>
<li><strong>编码实现</strong></li>
</ol>
<p>这套方法论的框架,是解决绝大多数代码问题的基本步骤。本课时,我们将在一些更开放的题目中进行演练,继续训练你的算法思维。</p>
<h3>算法思维训练题</h3>
<h4>例题 1斐波那契数列</h4>
<p>斐波那契数列是01123581321345589144……。你会发现这个数列中元素的性质是某个数等于它前面两个数的和也就是 a[n+2] = a[n+1] + a[n]。至于起始两个元素,则分别为 0 和 1。<strong>在这个数列中的数字,就被称为斐波那契数</strong></p>
<p><strong>【题目】写一个函数,输入 x输出斐波那契数列中第 x 位的元素</strong>。例如,输入 4输出 2输入 9输出 21。要求需要用递归的方式来实现。</p>
<p><strong>【解析】</strong> 在本课时开头,我们复习了解决代码问题的方法论,下面我们按照解题步骤进行详细分析。</p>
<ul>
<li>首先我们还是先做好复杂度的分析</li>
</ul>
<p>题目中要求要用递归的方式来实现,而递归的次数与 x 的具体数值有非常强的关系。因此,此时的时间复杂度应该是关于输入变量 x 的数值大小的函数。</p>
<ul>
<li>至于问题定位</li>
</ul>
<p>因为题目中已经明确了要采用递归去解决。所以也不用再去做额外的分析和判断了。</p>
<p>那么,如何使用递归呢?我们需要依赖斐波那契数列的重要性质“某个数等于它前面两个数的和”。也就是说,要求出某个位置 x 的数字,需要先求出 x-1 的位置是多少和 x-2 的位置是多少。递归同时还需要终止条件,对应于斐波那契数列的性质,就是起始两个元素,分别为 0 和 1。</p>
<ul>
<li>数据操作方面</li>
</ul>
<p>斐波那契数列需要对数字进行求和。而且所有的计算,都是依赖最原始的 0 和 1 进行。因此,这道题是不需要设计什么复杂的数据结构的。</p>
<ul>
<li>最后,实现代码</li>
</ul>
<p>我们围绕递归的性质进行开发,去试着写出递归体和终止条件。代码如下:</p>
<pre><code>public static void main(String[] args) {
int x = 20;
System.out.println(fun(x));
}
private static int fun(int n) {
if (n == 1) {
return 0;
}
if (n == 2) {
return 1;
}
return fun(n - 1) + fun(n - 2);
}
</code></pre>
<p><strong>下面,我们来对代码进行解读</strong></p>
<p><strong>主函数中</strong>,第 1 行到第 4 行,定义输入变量 x并调用 fun(x) 去计算第 x 位的斐波那契数列元素。</p>
<p><strong>在 fun() 函数内部</strong>,采用了递归去完成计算。递归分为递归体和终止条件:</p>
<ul>
<li>递归体是第 13 行。即当输入变量 n 比 2 大的时候,递归地调用 fun() 函数,并传入 n-1 和 n-2即 return fun(n - 1) + fun(n - 2)</li>
<li>终止条件则是在第 7 行到第 12 行,分别定义了当 n 为 1 或 2 的时候,直接返回 0 或 1。</li>
</ul>
<h4>例题2判断一个数组中是否存在某个数</h4>
<p><strong>【题目】给定一个经过任意位数的旋转后的排序数组,判断某个数是否在里面</strong></p>
<p>例如,对于一个给定数组 {4, 5, 6, 7, 0, 1, 2},它是将一个有序数组的前三位旋转地放在了数组末尾。假设输入的 target 等于 0则输出答案是 4即 0 所在的位置下标是 4。如果输入 3则返回 -1。</p>
<p><strong>【解析】</strong> 这道题目依旧是按照解决代码问题的方法论的步骤进行分析。</p>
<ul>
<li>先做复杂度分析</li>
</ul>
<p>这个问题就是判断某个数字是否在数组中,因此,复杂度极限就是全部遍历地去查找,也就是 O(n) 的复杂度。</p>
<ul>
<li>接着,进入定位问题的环节中</li>
</ul>
<p>这个问题有很多关键字,因此能够让你立马锁定问题。例如,判断某个数是否在数组里面,这就是一个查找问题。</p>
<ul>
<li>然后,我们来做数据操作分析</li>
</ul>
<p>原数组是经过某些处理的排序数组,也就是说原数组是有序的。有序和查找,你就会很快地想到,这个问题极有可能用二分查找的方式去解决,时间复杂度是 O(logn),相比上面 O(n) 的基线也是有显著的提高。</p>
<p>在利用二分查找时,更多的是判断,基本没有数据的增删操作,因此不需要太多地定义复杂的数据结构。</p>
<p>分析到这里,解决方案已经非常明朗了,就是采用二分查找的方法,在 O(logn) 的时间复杂度下去解决这个问题。二分查找可以通过递归来实现。<strong>而每次递归的关键点在于,根据切分的点(最中间的那个数字),确定是向左走还是向右走。这也是这个例题中唯一的难点了</strong></p>
<p>试想一下,在一个旋转后的有序数组中,利用中间元素作为切分点得到的两个子数组有什么样的性质。经过枚举不难发现,这两个子数组中,一定存在一个数组是有序的。也可能出现一个极端情况,二者都是有序的。如下图所示:</p>
<p><img src="assets/CgqCHl8Fi6eAVyX3AAAnk9vJF3c337.png" alt="Drawing 0.png" /></p>
<p>对于有序的一边,我们是很容易判断目标值,是否在这个区间内的。如果在其中,也说明了目标值不在另一边的旋转有序组里;反之亦然。</p>
<p>当我们知道了目标值在左右哪边之后,就可以递归地调用旋转有序的二分查找了。之所以可以递归调用,是因为,对于旋转有序组,这个问题和原始问题完全一致,可以调用。对于有序组,它是旋转有序的特殊情况(即旋转 0 位),也一定是可以通过递归的方法去实现查找的。直到不断二分后,搜索空间只有 1 位数字,直接判断是否找到即可。</p>
<ul>
<li>最后,实现代码</li>
</ul>
<p>我们给出这个例子的实现代码,如下:</p>
<pre><code>public static void main(String[] args) {
int[] arr = { 4, 5, 6, 7, 0, 1, 2 };
int target = 7;
System.out.println(bs(arr, target, 0, arr.length-1));
}
private static int bs(int[] arr, int target, int begin, int end) {
if (begin == end) {
if (target == arr[begin]){
return begin;
}
else{
return -1;
}
}
int middle = (begin + end)/2;
if (target == arr[middle]) {
return middle;
}
if (arr[begin] &lt;= arr[middle-1]){
if (arr[begin] &lt;= target &amp;&amp; target &lt;= arr[middle-1]) {
return bs(arr,target, begin,middle-1);
} else {
return bs(arr,target, middle+1,end);
}
}
else {
if (arr[middle+1] &lt;= target &amp;&amp; target &lt;= arr[end]) {
return bs(arr,target, middle+1,end);
} else {
return bs(arr,target, begin,middle-1);
}
}
}
</code></pre>
<p><strong>我们对代码进行解读:</strong></p>
<p><strong>主函数中,第 2 到 4 行。定义数组和 target并且执行二分查找</strong>。二分查找包括两部分,其一是二分策略,其二是终止条件。</p>
<p><strong>二分策略在代码的 1633 行:</strong></p>
<ul>
<li>16 行计算分裂点的索引值。17 到 19 行,进行目标值与分裂点的判断。
<ul>
<li>如果相等,则查找到结果并返回;</li>
<li>如果不等就要继续二分。</li>
</ul>
</li>
<li>在二分的过程中,第 20 行进行了左右子数组哪边是有序的判断。
<ul>
<li>如果左边有序,则进入到 21 到 25 行;</li>
<li>如果右边有序,则进入到 28 到 32 行。</li>
</ul>
</li>
<li>假设左边有序,则还需要判断 target 是否在有序区间内,这是在第 21 行。
<ul>
<li>如果在,则继续递归的调用 bs(arr,target, begin,middle-1)</li>
<li>如果不在有序部分,则说明 target 在另一边的旋转有序中,则调用 bs(arr,target, middle+1,end)。</li>
</ul>
</li>
</ul>
<p>下面的逻辑与此类似,不再赘述。</p>
<p><strong>经过了层层二分,最终 begin 和 end 变成了相等的两个变量,则进入到终止条件,即 8 到 15 行</strong></p>
<ul>
<li>在这里,需要判断最后剩下的 1 个元素是否与 target 相等:
<ul>
<li>如果相等则返回索引值;</li>
<li>如果不等则返回 -1。</li>
</ul>
</li>
</ul>
<h4>例题3求解最大公共子串</h4>
<p><strong>【题目】输入两个字符串,用动态规划的方法,求解出最大公共子串</strong></p>
<p>例如,输入 a = &quot;13452439&quot; b = &quot;123456&quot;。由于字符串&quot;345&quot;同时在 a 和 b 中出现,且是同时出现在 a 和 b 中的最长的子串。因此输出&quot;345&quot;</p>
<p><strong>【解析】这里已经定义了问题,就是寻找最大公共子串。同时也定义了方法,就是要用动态规划的方法</strong>。那么我们也不需要做太多的分析,只要依赖动态规划的步骤完成就可以了。</p>
<p>首先,我们回顾一下先前学过的最短路径问题。在最短路径问题中,我们是定义了起点和终点后,再去寻找二者之间的最短路径。</p>
<p>而现在的最大公共子串问题是,所有相邻的字符距离都是 1在不确定起点和终点时我们需要去寻找起点和终点之间最远的距离。</p>
<p>如果要基于已有的知识来探索陌生问题,那就需要根据每个可能的公共子串起点,去寻找与之对应的最远终点。这样就能得到全部的子串。随后再从中找到最大的那个子串。</p>
<p>别忘了,<strong>动态规划的基本方法是:分阶段、找状态、做决策、状态转移方程、定目标、寻找终止条件</strong>。下面我们来具体分析一下动态规划的步骤:</p>
<ul>
<li>对于一个可能的起点,它后面的每个字符都是一个阶段。</li>
<li>状态就是当前寻找到的相匹配的字符。</li>
<li>决策就是当前找到的字符是否相等(相等则进入到公共子串中)。</li>
<li>状态转移方程可以写作 sk+1 = uk(sk)。可以理解为,如果 sk = &quot;123&quot;是公共子串,且在 a 字符串和 b 字符串中,&quot;123&quot;后面的字符相等,假设为&quot;4&quot;则决策要进入到公共子串中sk+1 = &quot;1234&quot;</li>
<li>目标自然就是公共子串最长。</li>
<li>终止条件就是决策到了不相等的结果。</li>
</ul>
<p>这段分析对于初学者来说会非常难懂,接下来我们给一个实现的流程来辅助你理解。</p>
<p><strong>我们在最短路径问题中,曾重点提到的一个难点是,对于输入的图,采用什么样的数据结构予以保存。最终我们选择了二维数组</strong></p>
<p>在这个例子中也可以采用二维数组。每一行或每一列就对应了输入字符串 a 和 b 的每个字符,即 6 x 8 的二维数组(矩阵)为:</p>
<p><img src="assets/Ciqc1F8Fkz6APcxAAAAepje1Jv8882.png" alt="Drawing 2.png" /></p>
<p>接着,每个可能的起点字符,都应该同时出现在字符串 a 和 b 中,例如&quot;1&quot;就是一个可能的起点。如果以&quot;1&quot;作为起点,那么它后面的字符就是阶段,显然下个阶段就是 a[1] = 3 和 b[1] = 2。而此时的状态就是当前的公共子串&quot;1&quot;</p>
<p>决策的结果是,下一个阶段是否进入到公共子串中。很显然 a[1] 不等于 b[1],因此决策的结果是不进入。这也同时命中了终止条件。如果以&quot;3&quot;起点,则因为它之后的 a[2] 等于 b[3],则决策结果是进入到公共子串。</p>
<p>因此状态转移方程 sk+1 = uk(sk),含义是在&quot;3&quot;的状态下决策&quot;4&quot;进入子串,结果得到&quot;34&quot;。我们的目标是寻找最大的公共子串,因此可以用从 1 开始的数字定义距离(子串的长度)。具体步骤如下:</p>
<p>对于每个可能的起点,距离都是 1 (不可能的起点置为 0图中忽略未写。则有</p>
<p><img src="assets/CgqCHl8Fk3OAO0i4AAAfVRzJAfw184.png" alt="Drawing 4.png" /></p>
<p>接着利用状态转移方程,去寻找最优子结构。也就是,如果 b[i] = a[j],则 m[i,j] = m[i-1,j-1] + 1。含义为如果决策结果是相等则状态增加一个新的字符进行更新。可以得到</p>
<p><img src="assets/CgqCHl8Fk32AEA7dAAAgSbzrdRM560.png" alt="Drawing 6.png" /></p>
<p>最终,检索这个矩阵,得到的最大数字就是最大公共子串的长度。根据其所在的位置,就能从 a 或 b 中找到最大公共子串。</p>
<p>代码如下:</p>
<pre><code>public static void main(String[] args) {
String a = &quot;13452439&quot;;
String b = &quot;123456&quot;;
getCommenStr(a, b);
}
public static void getCommenStr(String a, String b) {
char[] c1 = a.toCharArray();
char[] c2 = b.toCharArray();
int[][] m = new int[c2.length+1][c1.length+1];
for (int i = 1; i &lt;= c2.length; i++) {
for (int j = 1; j &lt;= c1.length; j++) {
if (c2[i - 1] == c1[j - 1])
m[i][j] = m[i - 1][j - 1] + 1;
}
}
int max = 0;
int index = 0;
for (int i = 0; i &lt;= c2.length; i++) {
for (int j = 0; j &lt;= c1.length; j++) {
if (m[i][j] &gt; max) {
max = m[i][j];
index = i;
}
}
}
String s = &quot;&quot;;
for (int i = index - max; i &lt; index; i++)
s += b.charAt(i);
System.out.println(s);
}
</code></pre>
<p><strong>下面我们对代码进行解读:</strong></p>
<p>主函数中定义了字符串 a 和字符串 b随后调用动态规划代码。</p>
<p>进入 getCommenStr() 函数中之后,首先在第 10 行定义了二维数组。此时二维数组的维数是 7 x 9 的。这主要的原因是,后续会需要用到第一行和第一列的全零向量,作为起始条件。</p>
<p>接着,在第 1116 行,利用双重循环去完成状态转移的计算。此时就得到了最关键的矩阵,如下所示:</p>
<p><img src="assets/CgqCHl8Fk32AEA7dAAAgSbzrdRM560.png" alt="Drawing 6.png" /></p>
<p>随后的 1726 行,我们从矩阵 m 中,找到了最大值为 3在字符串 b 中的索引值为 4此时 index 为 5但别忘了我们之前额外定义了一行/一列的全零向量)。</p>
<p>最后2730 行,我们根据终点字符串索引值 4 和最大公共子串长度 3就能找到最大公共子串在 b 中的 24 的位置。即 &quot;345&quot;</p>
<h3>总结</h3>
<p>这一课时中,我们对例题做了详细的分析和讲解,重点其实是训练你的算法思维。为了检验你的学习成果,我们基于斐波那契数列的例题,再给出一个<strong>思考题,题目如下</strong></p>
<p><strong>如果现在是个线上实时交互的系统。客户端输入 x服务端返回斐波那契数列中的第 x 位。那么,这个问题使用上面的解法是否可行</strong></p>
<p>这里给你一个小提示,既然我这么问,答案显然是不可行的。如果不可行,原因是什么呢?我们又该如何解决?注意,题目中给出的是一个实时系统。当用户提交了 x如果在几秒内没有得到系统响应用户就会卸载 App 啦。</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/重学数据结构与算法-完/15 定位问题才能更好地解决问题:开发前的复杂度分析与技术选型.md.html">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/重学数据结构与算法-完/17 真题案例(二):数据结构训练.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":"70997de098203cfa","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>