learn.lianglianglee.com/专栏/重学数据结构与算法-完/17 真题案例(二):数据结构训练.md.html
2022-09-06 22:30:37 +08:00

406 lines
28 KiB
HTML
Raw 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>17 真题案例(二):数据结构训练.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 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 class="current-tab" 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>17 真题案例(二):数据结构训练</h1>
<p>在前面课时中,我们已经学习了解决代码问题的方法论。宏观上,它可以分为以下 4 个步骤:</p>
<ol>
<li><strong>复杂度分析</strong>。估算问题中复杂度的上限和下限。</li>
<li><strong>定位问题</strong>。根据问题类型,确定采用何种算法思维。</li>
<li><strong>数据操作分析</strong>。根据增、删、查和数据顺序关系去选择合适的数据结构,利用空间换取时间。</li>
<li><strong>编码实现</strong></li>
</ol>
<p>这套方法论的框架,是解决绝大多数代码问题的基本步骤。其中第 3 步,数据操作分析是数据结构发挥价值的地方。本课时,我们将继续通过经典真题案例进行数据结构训练。</p>
<h3>数据结构训练题</h3>
<h4>例题 1反转字符串中的单词</h4>
<p><strong>【题目】</strong> 给定一个字符串,逐个翻转字符串中的每个单词。例如,输入:&quot;This is a good example&quot;,输出:&quot;example good a is This&quot;。如果有多余的空格需要删除。</p>
<p><strong>【解析】</strong> 在本课时开头,我们复习了解决代码问题的方法论,下面我们按照解题步骤进行详细分析。</p>
<p><strong>首先分析一下复杂度</strong>。这里的动作可以分为拆模块和做翻转两部分。在采用比较暴力的方法时,拆模块使用一个 for 循环,做翻转也使用一个 for 循环。这样双重循环的嵌套,就是 O(n²) 的复杂度。</p>
<p><strong>接下来定位问题</strong>。我们可以看到它对数据的顺序非常敏感,敏感点一是每个单词需要保证顺序;敏感点二是所有单词放在一起的顺序需要调整为逆序。我们曾学过的关于数据顺序敏感的结构有队列和栈,也许这些结构可以适用在这个问题中。此处需要逆序,栈是有非常大的可能性被使用到的。</p>
<p><strong>然后我们进行数据操作分析</strong>。如果要使用栈的话,从结果出发,就需要按照顺序,把 This、is、a、good、example 分别入栈。要想把它们正确地入栈,就需要根据空格来拆分原始字符串。</p>
<p><strong>因此,经过分析后,这个例子的解法为:用空格把句子分割成单词。如果发现了多余的连续空格,需要做一些删除的额外处理。一边得到单词,一边把单词放入栈中。直到最后,再把单词从栈中倒出来,形成结果字符串</strong></p>
<p><img src="assets/Ciqc1F8MP8yAS72oABGrGx_blwA588.gif" alt="png" /></p>
<p><strong>最后,我们按照上面的思路进行编码开发</strong>。代码如下:</p>
<pre><code>public static void main(String[] args) {
String ss = &quot;This is a good example&quot;;
System.out.println(reverseWords(ss));
}
private static String reverseWords(String s) {
Stack stack=new Stack();
String temp = &quot;&quot;;
for (int i = 0; i &lt; s.length(); i++) {
if (s.charAt(i) != ' ') {
temp += s.charAt(i);
}
else if (temp != &quot;&quot;){
stack.push(temp);
temp = &quot;&quot;;
}
else{
continue;
}
}
if (temp != &quot;&quot;){
stack.push(temp);
}
String result = &quot;&quot;;
while (!stack.empty()){
result += stack.pop() + &quot; &quot;;
}
return result.substring(0,result.length()-1);
}
</code></pre>
<p><strong>下面我们对代码进行解读。</strong> 主函数中,第 14 行,不用过多赘述。第 7 行定义了一个栈,第 8 行定义了一个缓存字符串的变量。</p>
<p>接着,在第 920 行进入 for 循环。对每个字符分别进行如下判断:</p>
<ul>
<li>如果字符不是空格,当前单词还没有结束,则放在 temp 变量后面;</li>
<li>如果字符是空格1012 行),说明当前单词结束了;</li>
<li>如果 temp 变量不为空1316 行),则入栈;</li>
<li>如果字符是空格,但 temp 变量是空的就说明虽然单词结束了1719 行),但当前并没有得到新的单词。也就是连续出现了多个空格的情况。此时用 continue 语句忽略。</li>
</ul>
<p>然后,再通过 2123 行,把最后面的一个单词(它可能没有最后的空格帮助切分)放到栈内。此时所有的单词都完成了入栈。</p>
<p>最后,在 2428 行,让栈内的字符串先后出栈,并用空格隔离开放在 result 字符串内。最后返回 result 变量。别忘了,最后一次执行 pop 语句时,多给了 result 一个空格,需要将它删除掉。这样就完成了这个问题。</p>
<p><strong>这段代码采用了一层的 for 循环,显然它的时间复杂度是 O(n)。相比较于比较暴力的解法,它之所以降低了时间复杂度,就在于它开辟了栈的存储空间。所以空间复杂度也是 O(n)</strong></p>
<h4>例题 2树的层序遍历</h4>
<p><strong>【题目】</strong> 给定一棵树,按照层次顺序遍历并打印这棵树。例如,输入的树为:</p>
<p><img src="assets/CgqCHl8MP_WAERuIAACStyOKMQk754.png" alt="png" /></p>
<p>则打印 16、13、20、10、15、22、21、26。格外需要注意的是这并不是前序遍历。</p>
<p><strong>【解析】</strong> 如果你一直在学习这门课的话,一定对这道题目似曾相识。它是我们在 09 课时中留下的练习题。同时它也是高频面试题。仔细分析下这个问题,不难发现它是一个关于树的遍历问题。理论上是可以在 O(n) 时间复杂度下完成访问的。</p>
<p>以往我们学过的遍历方式有前序、中序和后序遍历,它们的实现方法都是通过递归。以前序遍历为例,递归可以理解为,先解决根结点,再解决左子树一边的问题,最后解决右子树的问题。这很像是在用深度优先的原则去遍历一棵树。</p>
<p>现在我们的问题要求是按照层次遍历,这就跟上面的深度优先的原则完全不一样了,更像是广度优先。也就是说,从遍历的顺序来看,一会在左子树、一会在右子树,会来回跳转。显然,这是不能用递归来处理的。</p>
<p>那么我们该如何解决呢?</p>
<p>我们从结果来看看这个问题有什么特点。打印的结果是 16、13、20、10、15、22、21、26。</p>
<p>从后往前看,可以发现:打印 21 和 26 之前,会先打印 22。这是一棵树的上下级关系打印 10 和 15 之前,会先打印 13这也是一棵树的上下级关系。显然结果对上下级关系的顺序非常敏感。</p>
<p>接着,我们发现 13 和 10、15 之间的打印关系并不连续,夹杂着右边的结点 20。也就是说左边的优先级大于右边大于下边。</p>
<p>分析到这里,你应该能找到一些感觉了吧。一个结果序列对顺序敏感,而且没有逆序的操作,满足这些特点的数据结构只有队列。所以我们猜测这个问题的解决方案,极有可能要用到队列。</p>
<p>队列只有入队列和出队列的操作。如果输出结果就是出队列的顺序,那这个顺序必然也是入队列的顺序,原因就在于队列的出入原则是先进先出。而入队列的原则是,上层父节点先进,左孩子再进,右孩子最后进。</p>
<p><strong>因此,这道题目的解决方案就是,根结点入队列,随后循环执行结点出队列并打印结果,左孩子入队列,右孩子入队列。直到队列为空</strong>。如下图所示:</p>
<p><img src="assets/CgqCHl8MQA2AWELaAA_8m3_f-_Q592.gif" alt="png" /></p>
<p>这个例子的代码如下:</p>
<pre><code>public static void levelTraverse(Node root) {
LinkedList&lt;Node&gt; queue = new LinkedList&lt;Node&gt;();
Node current = null;
queue.offer(root); // 根结点入队
while (!queue.isEmpty()) {
current = queue.poll(); // 出队队头元素
System.out.print(current.data);
// 左子树不为空,入队
if (current.leftChild != null)
queue.offer(current.leftChild);
// 右子树不为空,入队
if (current.rightChild != null)
queue.offer(current.rightChild);
}
}
</code></pre>
<p><strong>下面我们对代码进行解读</strong>。在这段代码中,第 2 行首先定义了一个队列 queue并在第 4 行让根结点入队列,此时队列不为空。</p>
<p>接着进入一个 while 循环进行遍历。当队列不为空的时候,第 6 行首先执行出队列操作,并把结果存在 current 变量中。随后第 7 行打印 current 的数值。如果 current 还有左孩子或右孩子,则分别按顺序执行入队列的操作,这是在第 913 行。</p>
<p>经过这段代码,可以完成的是,所有顺序都按照层次顺序入队列,且左孩子优先。这样就得到了按行打印的结果。时间复杂度是 O(n)。空间复杂度由于定义了 queue 变量,因此也是 O(n)。</p>
<h4>例题 3查找数据流中的中位数</h4>
<p><strong>【题目】</strong> 在一个流式数据中,查找中位数。如果是偶数个,则返回偏左边的那个元素。</p>
<p>例如:</p>
<p>输入 1服务端收到 1返回 1。</p>
<p>输入 2服务端收到 1、2返回 1。</p>
<p>输入 0服务端收到 0、1、2返回 1。</p>
<p>输入 20服务端收到 0、1、2、20返回 1。</p>
<p>输入 10服务端收到 0、1、2、10、20返回 2。</p>
<p>输入 22服务端收到 0、1、2、10、20、22返回 2。</p>
<p><strong>【解析】</strong> 这道题目依旧是按照解决代码问题的方法论的步骤进行分析。</p>
<p><strong>先看一下复杂度</strong>。显然,这里的问题定位就是个查找问题。对于累积的客户端输入,查找其中位数。中位数的定义是,一组数字按照从小到大排列后,位于中间位置的那个数字。</p>
<p>根据这个定义,最简单粗暴的做法,就是对服务端收到的数据进行排序得到有序数组,再通过 index 直接取出数组的中位数。排序选择快排的时间复杂度是 O(nlogn)。</p>
<p><strong>接下来分析一下这个查找问题</strong>。该问题有一个非常重要的特点,我们注意到,上一轮已经得到了有序的数组,那么这一轮该如何巧妙利用呢?</p>
<p>举个例子,如果采用全排序的方法,那么在第 n 次收到用户输入时,则需要对 n 个数字进行排序并输出中位数,此时服务端已经保存了这 n 个数字的有序数组了。而在第 n+1 次收到用户输入时,是不需要对 n+1 个数字整体排序的,仅仅通过插入这个数字到一个有序数组中就可以完成排序。显然,利用这个性质后,时间复杂度可以降低到 O(n)。</p>
<p><strong>接着,我们从数据的操作层面来看,是否仍然有优化的空间</strong>。对于这个问题,其目标是输出中位数。只要你能在 n 个数字中,找到比 x 小的 n/2 个数字和比 x 大的 n/2 个数字,那么 x 就是最终需要返回的结果。</p>
<p>基于这个思想,可以动态的维护一个最小的 n/2 个数字的集合,和一个最大的 n/2 个数字的集合。如果数字是奇数个,就我们就在左边最小的 n/2 个数字集合中多存一个元素。</p>
<p>例如,当前服务端收到的数字有 0、1、2、10、20。如果用两个数据结构分别维护 0、1、2 和 10、20那么当服务端收到 22 时,就可以根据 1、2、10 和 22 的大小关系,判断出中位数到底是多少了。</p>
<p>具体而言,当前的中位数是 2额外增加一个数字之后新的中位数只可能发生在 1、2、10 和新增的一个数字之间。不管中位数发生在哪里,都可以通过一些 if-else 语句进行查找,那么时间复杂度就是 O(1)。</p>
<p>虽然这种方法对于查找中位数的时间复杂度降低到了 O(1),但是它还需要有一些后续的处理,这主要是辅助下一次的请求。</p>
<p><strong>例如,当前用两个数据结构分别维护着 0、1、2 和 10、20那么新增了 22 之后,这两个数据结构如何更新。这就是原问题最核心的瓶颈了</strong></p>
<p>从结果来看,如果新增的数字比较小,那么就添加到左边的数据结构,并且把其中最大的 2 新增到右边,以保证二者数量相同。如果新增的数字比较大,那么就放到右边的数据结构,以保证二者数量相同。在这里,可能需要的数据操作包括,查找、中间位置的新增、最后位置的删除。</p>
<p>顺着这个思路继续分析,有序环境中的查找可以采用二分查找,时间复杂度是 O(logn)。最后位置的删除,并不牵涉到数据的挪移,时间复杂度是 O(1)。中间位置的新增就麻烦了,它需要对数据进行挪移,时间复杂度是 O(n)。如果要降低它的复杂度就需要用一些其他手段了。</p>
<p><strong>在这个问题中,有一个非常重要的信息,那就是题目只要中位数,而中位数左边和右边是否有序不重要。于是,我们需要用到这样的数据结构,大顶堆和小顶堆</strong></p>
<ul>
<li>大顶堆是一棵完全二叉树,它的性质是,父结点的数值比子结点的数值大;</li>
<li>小顶堆的性质与此相反,父结点的数值比子结点的数值小。</li>
</ul>
<p>有了这两个堆之后,我们的操作步骤就是,将中位数左边的数据都保存在大顶堆中,中位数右边的数据都保存在小顶堆中。同时,还要保证两个堆保存的数据个数相等或只差一个。这样,当有了一个新的数据插入时,插入数据的时间复杂度是 O(logn)。而插入后的中位数,肯定在大顶堆的堆顶元素上,因此,找到中位数的时间复杂度就是 O(1)。</p>
<p>我们把这个思路,用代码来实现,则有:</p>
<pre><code>import java.util.PriorityQueue;
import java.util.Comparator;
public class testj {
int count = 0;
static PriorityQueue&lt;Integer&gt; minHeap = new PriorityQueue&lt;&gt;();
static PriorityQueue&lt;Integer&gt; maxHeap = new PriorityQueue&lt;&gt;(new Comparator&lt;Integer&gt;() {
@Override
public int compare(Integer o1, Integer o2) {
return o2.compareTo(o1);
}
});
public void Insert(Integer num) {
if (count % 2 == 0) {
minHeap.offer(num);
maxHeap.offer(minHeap.poll());
} else {
maxHeap.offer(num);
minHeap.offer(maxHeap.poll());
}
count++;
System.out.println(testj.GetMedian());
}
public static int GetMedian() {
return maxHeap.peek();
}
public static void main(String[] args) {
testj t = new testj();
t.Insert(1);
t.Insert(2);
t.Insert(0);
t.Insert(20);
t.Insert(10);
t.Insert(22);
}
}
</code></pre>
<p><strong>我们对代码进行解读:</strong> 在第 612 行,分别定义了最小堆和最大堆。第 5 行的变量,保存的是累积收到的输入个数,可以用来判断奇偶。接着我们看主函数的第 3038 行。在这里,模拟了流式数据,先后输入了 1、2、0、20、10、22并调用了 Inset() 函数。</p>
<p>从第 14 行开始Inset() 函数中,需要判断 count 的奇偶性:如果 count 是偶数,则新的数据需要先加入最小堆,再弹出最小堆的堆顶,最后把弹出的数据加入最大堆。如果 count 是奇数,则新的数据需要先加入最大堆,再弹出最大堆的堆顶,最后把弹出的数据加入最小堆。</p>
<p>执行完后count 加 1。然后调用 GetMedian() 函数来寻找中位数GetMedian() 函数通过 27 行直接返回最大堆的对顶,这是因为我们约定中位数在偶数个的时候,选择偏左的元素。</p>
<p>最后,我们给出插入 22 的执行过程,如下图所示:</p>
<p><img src="assets/Ciqc1F8MQD2AGebMAAm_13LKPTk687.gif" alt="png" /></p>
<h3>总结</h3>
<p>这一课时主要围绕数据结构展开问题的分析和讨论。对于树的层次遍历,我们再拓展一下。</p>
<p>如果要打印的不是层次,而是蛇形遍历,又该如何实现呢?蛇形遍历就是 s 形遍历,即奇数层从左到右,偶数层从右到左。如果是例题 2 的树,则蛇形遍历的结果就是 16、20、13、10、15、22、26、21。我们就把这个问题当作本课时的练习题。</p>
</div>
</div>
<div>
<div style="float: left">
<a href="/专栏/重学数据结构与算法-完/16 真题案例(一):算法思维训练.md.html">上一页</a>
</div>
<div style="float: right">
<a href="/专栏/重学数据结构与算法-完/18 真题案例(三):力扣真题训练.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":"70997de2fe283cfa","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>