杨子鳄鱼 ● 外贸自建站

十年外贸建站经验,专注外贸独立站建设,欢迎咨询

0%

人心本無染,心靜自然清。

语法规则

1
2
location [ = | ~ | ~* | ^~ ] uri { ... }
location @name { ... }

语法规则很简单,一个location关键字,后面跟着可选的修饰符,后面是要匹配的字符,花括号中是要执行的操作。

修饰符

  • = 表示精确匹配。只有请求的url路径与后面的字符串完全相等时,才会命中。
  • ~ 表示该规则是使用正则定义的,区分大小写。
  • ~* 表示该规则是使用正则定义的,不区分大小写。
  • ^~ 表示如果该符号后面的字符是最佳匹配,采用该规则,不再进行后续的查找。

匹配过程

对请求的url序列化。例如,对%xx等字符进行解码,去除url中多个相连的/,解析url中的.,..等。这一步是匹配的前置工作。

location有两种表示形式,一种是使用前缀字符,一种是使用正则。如果是正则的话,前面有~~*修饰符。

具体的匹配过程如下:

首先先检查使用前缀字符定义的location,选择最长匹配的项并记录下来。

如果找到了精确匹配的location,也就是使用了=修饰符的location,结束查找,使用它的配置。

然后按顺序查找使用正则定义的location,如果匹配则停止查找,使用它定义的配置。

如果没有匹配的正则location,则使用前面记录的最长匹配前缀字符location。

基于以上的匹配过程,我们可以得到以下两点启示:

使用正则定义的location在配置文件中出现的顺序很重要。因为找到第一个匹配的正则后,查找就停止了,后面定义的正则就是再匹配也没有机会了。
使用精确匹配可以提高查找的速度。例如经常请求/的话,可以使用=来定义location。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
location = / {
[ configuration A ]
}

location / {
[ configuration B ]
}

location /user/ {
[ configuration C ]
}

location ^~ /images/ {
[ configuration D ]
}

location ~* \.(gif|jpg|jpeg)$ {
[ configuration E ]
}

请求/精准匹配A,不再往下查找。

请求/index.html匹配B。首先查找匹配的前缀字符,找到最长匹配是配置B,接着又按照顺序查找匹配的正则。结果没有找到,因此使用先前标记的最长匹配,即配置B。

请求/user/index.html匹配C。首先找到最长匹配C,由于后面没有匹配的正则,所以使用最长匹配C。
请求/user/1.jpg匹配E。首先进行前缀字符的查找,找到最长匹配项C,继续进行正则查找,找到匹配项E。因此使用E。

请求/images/1.jpg匹配D。首先进行前缀字符的查找,找到最长匹配D。但是,特殊的是它使用了^~修饰符,不再进行接下来的正则的匹配查找,因此使用D。这里,如果没有前面的修饰符,其实最终的匹配是E。大家可以想一想为什么。

请求/documents/about.html匹配B。因为B表示任何以/开头的URL都匹配。在上面的配置中,只有B能满足,所以匹配B。

location @name的用法

@用来定义一个命名location。主要用于内部重定向,不能用来处理正常的请求。其用法如下:

1
2
3
4
5
6
location / {
try_files $uri $uri/ @custom
}
location @custom {
# ...do something
}

上例中,当尝试访问url找不到对应的文件就重定向到我们自定义的命名location(此处为custom)。

值得注意的是,命名location中不能再嵌套其它的命名location。

URL尾部的/需不需要

心有猛虎,细嗅蔷薇。

查看ngnix是否安装了http-real-ip模块

1
nginx -V

创建文件cloudflare

https://www.cloudflare.com/ips/

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
vi /etc/nginx/cloudflare;


#Cloudflare ip addresses

# - IPv4
set_real_ip_from 103.21.244.0/22;
set_real_ip_from 103.22.200.0/22;
set_real_ip_from 103.31.4.0/22;
set_real_ip_from 104.16.0.0/12;
set_real_ip_from 108.162.192.0/18;
set_real_ip_from 131.0.72.0/22;
set_real_ip_from 141.101.64.0/18;
set_real_ip_from 162.158.0.0/15;
set_real_ip_from 172.64.0.0/13;
set_real_ip_from 173.245.48.0/20;
set_real_ip_from 188.114.96.0/20;
set_real_ip_from 190.93.240.0/20;
set_real_ip_from 197.234.240.0/22;
set_real_ip_from 198.41.128.0/17;

# - IPv6
set_real_ip_from 2400:cb00::/32;
set_real_ip_from 2405:8100::/32;
set_real_ip_from 2405:b500::/32;
set_real_ip_from 2606:4700::/32;
set_real_ip_from 2803:f800::/32;
set_real_ip_from 2c0f:f248::/32;
set_real_ip_from 2a06:98c0::/29;

real_ip_header CF-Connecting-IP;

修改 /etc/nginx/nginx.conf

Open “/etc/nginx/nginx.conf” file with your favorite text editor and just add the following lines to your nginx.conf inside http{….} block.

1
2

include /etc/nginx/cloudflare;

重启

1
nginx -s reload

做你害怕做的事情。

「Hotlink Protection」(直接連結保護)是經營網站經常需要去注意的一塊,但為什麼我們會需要「Hotlink Protection」呢?身為圖文並茂的網路文章作家,最擔心得就是自己的文章被別人整篇連文帶圖地複製貼上到其它地方了。此時如果圖片有套用「Hotlink Protection」的話,就可以讓被盜用的圖片在其它網站上「不被正常顯示」出來,如此一來,就能使其它誤入盜文頁面的訪客可以知道該篇文章是篇被盜用的文章。不同的網站架構有不同的「Hotlink Protection」的設定方式,如果您的網站是使用Nginx伺服器來架設的話,可以參考這篇文章,來實現「Hotlink Protection」的功能。

Nginx的ngx_http_referer_module模組

「ngx_http_referer_module」是Nginx預設啟用的模組,可以用來阻擋使用無效HTTP標頭的Referer欄位的請求。使用「ngx_http_referer_module」模組時,通常會在Nginx設定檔中的特定location區塊內撰寫「valid_referers」命令,來針對特定連結做檢查HTTP標頭中Referer欄位的動作。

例如:

1
2
3
4
5
6
location ~ \.(jpg|jpeg|png|gif)$ {
valid_referers none server_names blocked *.magiclen.org *.facebook.com;
if ($invalid_referer) {
return 403;
}
}

先從location ~ \.(jpg|jpeg|png|gif)$開始介紹起。撰寫於server區塊內的location區塊,可以使Nginx伺服器能針對特定格式的網址來做不同的動作。至於這個「特定格式的網址」的網址規則,則是直接接在location這個命令之後。此處的~,表示要使用正規表示式,且必須判斷大小寫(case-sensitive)。而\.(jpg|jpeg|png|gif)$這串正規表示式,則代表所有以.jpg、.jpeg、.png或.gif結尾的網址。您可以將需要加上「Hotlink Protection」的圖片副檔名都放進這個正規表示式內。

再來看一下valid_referers命令,valid_referers命令可以用來設定$invalid_referer變數的值。若請求中的HTTP標頭的Referer欄位有符合valid_referers命令所指定的條件,此時$invalid_referer變數的值為空字串,所以之後的if判斷式就不會成立;若Referer欄位不符合valid_referers命令所指定的條件,則此時$invalid_referer變數的值為1,所以之後的if判斷式就會成立,而使得該連結會直接回傳HTTP的403狀態(Forbidden)。

至於valid_referers命令所接受的參數,有以下幾個:

  • none:這個參數可以允許那些HTTP標頭中沒有Referer欄位的請求。
  • server_names:這個參數可以允許那些HTTP標頭中的Referer欄位值符合server_name命令所設定的名稱的請求。
  • blocked:這個參數可以允許那些HTTP標頭中的Referer欄位值是空字串的請求。之所以遇到Referer欄位值是空字串的情況,通常是因為防火牆或是Proxy伺服器把原先HTTP標頭中的Referer欄位值給擋下來了。
  • 伺服器名稱:可以直接填入多個伺服器名稱(同樣以空格分隔),來作為判斷Referer欄位值是否有效的依據,名稱支援星號(*),表示任意數量的任意字元。
  • 正規表示式:可以以~字元為開頭再接上正規表示式,來判斷請求HTTP標頭中的Referer欄位值是否符合指定的正規表示式。

如果想要將無效的請求轉到其它網址,需要修改return命令,可以參考以下連結:

https://magiclen.org/nginx-rewrite

如果網站有使用CDN的話,本篇文章的作法就不怎麼合適了。若是Cloudflare CDN的話,可以參考以下連結來設定:

https://magiclen.org/cloudflare-hotlink-protection/

https://magiclen.org/nginx-hotlink-protection/

“你只见到这么多人说他不好时, 却看到他有出来说別人一句吗?”

MutationObserver 是一个内建对象,它观察 DOM 元素,在其发生更改时触发回调。
我们将首先看一下语法,然后探究一个实际的用例,以了解它在什么地方有用。

语法

MutationObserver 使用简单。
首先,我们创建一个带有回调函数的观察器:

1
let observer = new MutationObserver(callback);

然后将其附加到一个 DOM 节点:

1
observer.observe(node, config);

config 是一个具有布尔选项的对象,该布尔选项表示“将对哪些更改做出反应”:

  • childList —— node 的直接子节点的更改,
  • subtree —— node 的所有后代的更改,
  • attributes —— node 的特性(attribute),
  • attributeFilter —— 特性名称数组,只观察选定的特性。
  • characterData —— 是否观察 node.data(文本内容),

其他几个选项:

  • attributeOldValue —— 如果为 true,则将特性的旧值和新值都传递给回调(参见下文),否则只传新值(需要 attributes 选项),
  • characterDataOldValue —— 如果为 true,则将 node.data 的旧值和新值都传递给回调(参见下文),否则只传新值(需要 characterData 选项)。
    然后,在发生任何更改后,将执行“回调”:更改被作为一个 MutationRecord 对象列表传入第一个参数,而观察器自身作为第二个参数。

MutationRecord 对象具有以下属性:

type —— 变动类型,以下类型之一:
“attributes”:特性被修改了,
“characterData”:数据被修改了,用于文本节点,
“childList”:添加/删除了子元素。
target —— 更改发生在何处:”attributes” 所在的元素,或 “characterData” 所在的文本节点,或 “childList” 变动所在的元素,
addedNodes/removedNodes —— 添加/删除的节点,
previousSibling/nextSibling —— 添加/删除的节点的上一个/下一个兄弟节点,
attributeName/attributeNamespace —— 被更改的特性的名称/命名空间(用于 XML),
oldValue —— 之前的值,仅适用于特性或文本更改,如果设置了相应选项 attributeOldValue/characterDataOldValue。

例如,这里有一个

,它具有 contentEditable 特性。该特性使我们可以聚焦和编辑元素。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<div contentEditable id="elem">Click and <b>edit</b>, please</div>

<script>
let observer = new MutationObserver(mutationRecords => {
console.log(mutationRecords); // console.log(the changes)
});

// 观察除了特性之外的所有变动
observer.observe(elem, {
childList: true, // 观察直接子节点
subtree: true, // 及其更低的后代节点
characterDataOldValue: true // 将旧的数据传递给回调
});
</script>

如果我们在浏览器中运行上面这段代码,并聚焦到给定的 <div> 上,然后更改 <b>edit</b> 中的文本,console.log 将显示一个变动:

1
2
3
4
5
6
mutationRecords = [{
type: "characterData",
oldValue: "edit",
target: <text node>,
// 其他属性为空
}];

如果我们进行更复杂的编辑操作,例如删除 edit,那么变动事件可能会包含多个变动记录:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
mutationRecords = [{
type: "childList",
target: <div#elem>,
removedNodes: [<b>],
nextSibling: <text node>,
previousSibling: <text node>
// 其他属性为空
}, {
type: "characterData"
target: <text node>
// ...变动的详细信息取决于浏览器如何处理此类删除
// 它可能是将两个相邻的文本节点 "edit " 和 ", please" 合并成一个节点,
// 或者可能将它们留在单独的文本节点中
}];

因此,MutationObserver 允许对 DOM 子树中的任何更改作出反应。

用于集成

在什么时候可能有用?
想象一下,你需要添加一个第三方脚本,该脚本不仅包含有用的功能,还会执行一些我们不想要的操作,例如显示广告 <div class="ads">Unwanted ads</div>
当然,第三方脚本没有提供删除它的机制。
使用 MutationObserver,我们可以监测到我们不需要的元素何时出现在我们的 DOM 中,并将其删除。
还有一些其他情况,例如第三方脚本会将某些内容添加到我们的文档中,并且我们希望检测出这种情况何时发生,以调整页面,动态调整某些内容的大小等。
MutationObserver 使我们能够实现这种需求。

用于架构

从架构的角度来看,在某些情况下,MutationObserver 有不错的作用。
假设我们正在建立一个有关编程的网站。自然地,文章和其他材料中可能包含源代码段。

在HTML 标记(markup)中的此类片段如下所示:

1
2
3
4
5
6
...
<pre class="language-javascript"><code>
// 这里是代码
let hello = "world";
</code></pre>
...

另外,我们还将在网站上使用 JavaScript 高亮显示库,例如 Prism.js。调用 Prism.highlightElem(pre) 会检查此类 pre 元素的内容,并在其中添加特殊标签(tag)和样式,以进行彩色语法高亮显示,类似于你在本文的示例中看到的那样。

那什么时候运行该高亮显示方法呢?我们可以在 DOMContentLoaded 事件中或者在页面尾部运行。到那时,我们的 DOM 已准备就绪,我们可以搜索元素 pre[class*="language"] 并对其调用 Prism.highlightElem:

1
2
// 高亮显示页面上的所有代码段
document.querySelectorAll('pre[class*="language"]').forEach(Prism.highlightElem);

到目前为止,一切都很简单,对吧?HTML 中有 <pre> 代码段,我们高亮显示它们。

现在让我们继续。假设我们要从服务器动态获取资料。我们将 在本教程的后续章节 中学习进行此操作的方法。目前,只需要关心我们从网络服务器获取 HTML 文章并按需显示

1
2
let article = /* 从服务器获取新内容 */
articleElem.innerHTML = article;

新的 article HTML 可能包含代码段。我们需要对其调用 Prism.highlightElem,否则它们将不会被高亮显示。

对于动态加载的文章,应该在何处何时调用 Prism.highlightElem?

我们可以将该调用附加到加载文章的代码中,如下所示:

1
2
3
4
5
let article = /* 从服务器获取新内容 */
articleElem.innerHTML = article;

let snippets = articleElem.querySelectorAll('pre[class*="language-"]');
snippets.forEach(Prism.highlightElem);

……但是,想象一下,代码中有很多地方可以加载内容:文章,测验,论坛帖子。我们是否需要在每个地方都附加一个高亮显示调用?那不太方便,也很容易忘记。

并且,如果内容是由第三方模块加载的,该怎么办?例如,我们有一个由其他人编写的论坛,该论坛可以动态加载内容,并且我们想为其添加语法高亮显示。没有人喜欢修补第三方脚本。

幸运的是,还有另一种选择。

我们可以使用 MutationObserver 来自动检测何时在页面中插入了代码段,并高亮显示之它们。

因此,我们在一个地方处理高亮显示功能,从而使我们无需集成它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
let observer = new MutationObserver(mutations => {

for(let mutation of mutations) {
// 检查新节点,有什么需要高亮显示的吗?

for(let node of mutation.addedNodes) {
// 我们只跟踪元素,跳过其他节点(例如文本节点)
if (!(node instanceof HTMLElement)) continue;

// 检查插入的元素是否为代码段
if (node.matches('pre[class*="language-"]')) {
Prism.highlightElement(node);
}

// 或者可能在子树的某个地方有一个代码段?
for(let elem of node.querySelectorAll('pre[class*="language-"]')) {
Prism.highlightElement(elem);
}
}
}

});

let demoElem = document.getElementById('highlight-demo');

observer.observe(demoElem, {childList: true, subtree: true});

下面有一个 HTML 元素,以及使用 innerHTML 动态填充它的 JavaScript。

请先运行前面那段代码(上面那段,观察元素),然后运行下面这段代码。你将看到 MutationObserver 是如何检测并高亮显示代码段的。

一个具有 id=”highlight-demo” 的示例元素,运行上面那段代码来观察它。

下面这段代码填充了其 innerHTML,这导致 MutationObserver 作出反应,并突出显示其内容:

1
2
3
4
5
6
7
8
9
10
let demoElem = document.getElementById('highlight-demo');

// 动态插入带有代码段的内容
demoElem.innerHTML = `下面是一个代码段:
<pre class="language-javascript"><code> let hello = "world!"; </code></pre>
<div>另一个代码段:</div>
<div>
<pre class="language-css"><code>.class { margin: 5px; } </code></pre>
</div>
`;

现在我们有了 MutationObserver,它可以跟踪观察到的元素中的,或者整个 document 中的所有高亮显示。我们可以在 HTML 中添加/删除代码段,而无需考虑高亮问题。

https://zh.javascript.info/mutation-observer

这是场接力赛,你却当成百米赛跑。

async

首先,我们使用async关键字,您可以将它放在函数声明之前,将其转换为async function。异步函数是一个知道怎样预期 await 关键字可用于调用异步代码可能性的函数。

尝试在浏览器的JS控制台中键入以下行:

1
2
function hello() { return "Hello" };
hello();

该函数返回“Hello” ––没什么特别的,对吧?

但是,如果我们将其变成异步函数呢?请尝试以下方法:

1
2
async function hello() { return "Hello" };
hello();

额。。现在调用该函数会返回一个promise。这是异步函数的特征之一 ––它将任何函数转换为promise。
你也可以创建一个异步函数表达式(参见async function expression),如下所示:

1
2
let hello = async function() { return "Hello" };
hello();

你可以使用箭头函数:

1
let hello = async () => { return "Hello" };

这些都基本上是一样的。

要实际使用promise实现时返回的值,因为它返回了一个promise,我们可以使用.then()块:

1
hello().then((value) => console.log(value))

甚至只是简写如

1
hello().then(console.log)

就像我们在上一篇文章中看到的那样
因此,将async关键字添加到函数中以告诉它们返回promise而不是直接返回值。此外,这使同步函数可以避免运行支持使用await时带来的任何潜在开销。通过仅在函数声明为异步时添加必要的处理,JavaScript引擎可以为您优化您的程序。爽!

await

将它与await关键字结合使用时,异步函数的真正优势就变得明显了。这可以放在任何基于异步声明的函数之前,暂停代码在该行上,直到promise完成,然后返回结果值。与此同时,其他正在等待执行机会的代码就有可能如愿执行了。
这是一个简单的示例:

1
2
3
4
5
async function hello() {
return greeting = await Promise.resolve("Hello");
};

hello().then(alert);

关键字 await 让 JavaScript 引擎等待直到 promise 完成(settle)并返回结果
这里的例子就是一个 1 秒后 resolve 的 promise:

1
2
3
4
5
6
7
8
9
10
11
12
async function f() {

let promise = new Promise((resolve, reject) => {
setTimeout(() => resolve("done!"), 1000)
});

let result = await promise; // 等待,直到 promise resolve (*)

alert(result); // "done!"
}

f();

这个函数在执行的时候,“暂停”在了 (*) 那一行,并在 promise settle 时,拿到 result 作为结果继续往下执行。所以上面这段代码在一秒后显示 “done!”

让我们强调一下:await 字面的意思就是让 JavaScript 引擎等待直到 promise settle,然后以 promise 的结果继续执行。这个行为不会耗费任何 CPU 资源,因为引擎可以同时处理其他任务:执行其他脚本,处理事件等。

相比于 promise.then,它只是获取 promise 的结果的一个更优雅的语法,同时也更易于读写。

多花一點心思去說溫柔的話,那些話可能改變他人一生

背景

使用net/http同时发起多个简单请求时,偶尔会出现EOF或connect: connection reset by peer的情况。明明就是一个很简单的例子,为何会出现这种情况呢?

举例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
eq, err := http.NewRequest(method, url, body)
if err !=nil{

return nil, err

}

resp, err := http.DefaultClient.Do(req)
if err !=nil{
return nil, err
}
defer resp.Body.Close()

b, err := ioutil.ReadAll(resp.Body)
if err !=nil{
return nil, err
}

return b,nil

如此简单的几行代码,在我们套上大并发以后,各种异常情况接踵而至,最常见的就是下面的几个:

  • EOF
  • connection reset by peer

探讨

HTTP也是针对TCP的一个封装,go得益于goroutine和channel,与其他语言的实现方式不太一样,它是直接起了两个协程,一个用于读(readLoop),一个用于写(writeLoop)。

conn.Read

  • socket无数据:read阻塞,直到有数据。
  • socket有部分数据:如果socket中有部分数据,且长度小于一次Read操作所期望读出的数据长度,那么Read将会成功读出这部分数据并返回,而不是等待所有期望数据全部读取后再返回。
  • socket有足够数据:如果socket中有数据,且长度大于等于一次Read操作所期望读出的数据长度,那么Read将会成功读出这部分数据并返回。这个情景是最符合我们对Read的期待的了:Read将用Socket中的数据将我们传入的slice填满后返回:n = 10, err = nil
  • 有数据,socket关闭:第一次Read成功读出了所有的数据,当第二次Read时,由于client端 socket关闭,Read返回EOF error;
  • 无数据,socket关闭:Read直接返回EOF error

conn.Write

  • 成功写:Write调用返回的n与预期要写入的数据长度相等,且error = nil;
  • 写阻塞:当发送方将对方的接收缓冲区以及自身的发送缓冲区写满后,Write就会阻塞;
  • 写入部分数据:Write操作存在写入部分数据的情况,没有按照预期的写入所有数据,则需要循环写入。

解决

通过上面的描述,我们大致已经明白了为何会出现EOF了,其实就是readLoop在进行读的时候,检测到socket被关闭了。在HTTP1.1,我们默认都是采用了Keep-Alive的,也就是会启动长连接,当server端断掉了该socket后,我们的EOF就出来了。所以尽量避免该情况发生的话,直接这样req.Close = true

https://dpjeep.com/golangzhi-http-eofxiang-jie/

すぐ目(め)の前(まえ)にあっても、手(て)に入れる事(こと)が出来(でき)ないものがある

应用场景

在cms 开发中,SEO优化 一般需要把文章标题 设置为 img 的alt title 属性,那么内容多个 img 时,手动过于繁琐。

实现原理

根据以上需求,那么使用正则找内容的 img 标签 ,进行添加或者替换即可

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
<?php
$str ='<div class="wkBody">
<div id="panelSummary" class="abstract">

<span class="font28">摘要:</span>
作为阿根廷种植面积第二大的红葡萄品种,伯纳达的发展潜力越来越大,逐渐成为阿根廷的希望之星。
<div id="pnlSummaryEN" class="abs-en">

<span class="font28">ABSTRACT:</span>
Bonarda is Argentina’s second most widely planted red grape, which may make a big splash in the coming years.

</div>

</div>
<p>
说到阿根廷葡萄酒,大家最先想到的可能是著名的<a href="http://www.wine-world.com/s/grape?q=马尔贝克&amp;token=ba782698-e5be-44f8-8e54-09eac16f02a9&amp;from=user" target="_blank">马尔贝克</a>(Malbec),毫无疑问,这个品种在阿根廷占据着绝对的主导地位。但是,除了马尔贝克以外,阿根廷还有一个发展潜力巨大的葡萄品种,它是阿根廷的希望之星——阿根廷种植面积第二大的葡萄品种伯纳达(Bonarda)。<br>
<br>
 <strong> 1. 特征</strong></p>
<p style="text-align: center;">
<strong><a target="_blank" href="/culture/pic/22571cf7-ee6b-4176-8efb-e02e4e9dd947/0"><img alt="除了马尔贝克,阿根廷还有伯纳达" title="除了马尔贝克,阿根廷还有伯纳达" src="/UserFiles/images/01-bonarda-161114.jpg" style="width: 375px; height: 500px;"></a></strong></p>
<p style="text-align: center;">
<span style="font-size:12px;">图片来源:Wine Folly</span></p>
<p>
伯纳达葡萄酒初闻时果香扑鼻,带有糖渍黑樱桃、新鲜蓝莓和李子的香气。紧接着,香气变得更加复杂,散发出紫罗兰、五香、多香果和牡丹的气息。最后,经过橡木桶的酒还会有轻微的雪茄盒、甜无花果和巧克力等烟熏气息。入口时,伯纳达葡萄酒最先表现的也是果味,酒体中等,鲜酸多汁,单宁较低,余味顺滑,十分易饮。<br>
<br>
  伯纳达与马尔贝克一样拥有饱满的颜色,但伯纳达的单宁含量更低,酸度更高、更多汁。如果你不喜欢使用橡木桶的葡萄酒,那你可能会青睐伯纳达葡萄酒,因为大多数伯纳达葡萄酒少用甚至不用橡木桶。另外,伯纳达葡萄酒的酒精度很少超过13.5%。<br>
<br>
 <strong> 2. 配餐</strong></p>
<p style="text-align: center;">
<strong><a target="_blank" href="/culture/pic/22571cf7-ee6b-4176-8efb-e02e4e9dd947/1"><img alt="除了马尔贝克,阿根廷还有伯纳达" title="除了马尔贝克,阿根廷还有伯纳达" src="/UserFiles/images/02-cedar-plank-salmon-161115.jpg" style="width: 550px; height: 333px;"></a><div style="width: 550px;" class="addivstyle"><div id="testdiv" style="width: 550px;" class="adseconddiv"><a href="http://mall.wine-world.com/activity/activitypromotion10" target="_blank"><img src="http://images.wine-world.com/images/wineworldlogo.jpg" style="left:2px;top:2px;width:102px;position: absolute;border: none;background: none;"><span style="position: absolute;left:120px;right:85px;top:10px;color: #FFF;text-align:left;">开抢50元代金券,醉惠中级庄盛宴<br>30款中级庄美酒任君挑选,领券后下单直减50元!</span><span class="qukankanstyle" style="top: 20px;">去看看</span></a></div></div></strong></p>
<p style="text-align: center;">
<span style="font-size:12px;">图片来源:Wine Folly</span></p>
<p>
伯纳达葡萄酒低单宁、高酸的特点使其可以与多种食物搭配,如鸡肉、牛肉、猪肉和鱼排等;而酒中轻微的棕色香料的风味也可以与菠萝、芒果、照烧汁等完美结合。<br>
<br>
<strong>  3. 此伯纳达非彼伯纳达</strong><br>
<br>
  阿根廷的伯纳达与其它地方的伯纳达很可能并不是同一个葡萄品种,也许阿根廷的伯纳达本不该被称为“伯纳达”。DNA检测表明阿根廷的伯纳达与法国萨瓦(Savoie)产区一种叫“Deuce Noir”的稀有葡萄是同一品种,跟纳帕谷(Napa Valley)古老葡萄园中的沙帮乐(Charbono)也是相同的品种。<br>
<br>
  实际上,真正的伯纳达葡萄指的是一组意大利葡萄,至少分为6种,其中最出名的是皮埃蒙特伯纳达(Bonarda Piemontese)。更容易引起混淆的是,意大利伦巴第(Lombardy)的奥尔特莱伯·帕韦斯(Oltrepo Pavese)产区所产的标有“伯纳达”的葡萄酒实际上是用科罗帝纳(Croatina)酿制而成的;<a href="http://www.wine-world.com/s/area?q=皮埃蒙特&amp;token=36d3dd49-745b-43f8-8bce-1b90ce3118b0&amp;from=user" target="_blank">皮埃蒙特</a>(Piedmont)有些酿酒师酿制的标有“伯纳达”的葡萄酒其实是用一种叫茹拉(Uva Rara)的葡萄酿制的。<br>
<br>
  所以,如果下次有人纠正你对阿根廷伯纳达的叫法时,不妨问问他们以上这两种标有“伯纳达”的葡萄酒为什么是用别的葡萄酿制的,他们最好能说出它们之间的联系。<span style="font-size:12px;">(文/Christina)</span></p>

<div id="pnlWineVersion2">

<p>声明:本文版权属于“红酒世界网”,转载请保留版权信息。关注微信号“wine-world”,随时随地了解最新红酒资讯。</p>

</div>
</div>';


$preg = "/<img.*?src=[\"|\'](.*?)[\"|\'].*?>/";
$alt = "1234";
$title = "123456";
$img = '<img src="$1" alt="'.$alt.'"title="'.$title.'">';
$data = preg_replace($preg,$img,$str);
var_dump($data);

https://segmentfault.com/a/1190000007505546

醉后不知天在水,满船清梦压星河

简述

单向链表属于链表的一种,也叫单链表,单向即是说它的链接方向是单向的,它由若干个节点组成,每个节点都包含下一个节点的指针。

其实关于链表,大家完全可以把他和数组进行类比学习,只不过数组由于申请的空间必须是连续地,所以有的时候内存地址可能分配不出足够的内存空间。链表的的内存空间可以不连续的特性就可以解决数组的痛点了。

对于链表与数组的使用场景就是,如果你的数据需要经常的查询,那么数组更快。如果你的数据需要频繁的插入删除,链表比数组更快。

  • 创建单链表时无需指定链表的长度,这个比起数组结构更加有优势,而数组纵使实现成动态数组也是需要指定一个更大的数组长度,而且要把原来的数组元素一个个复制到新数组中。
  • 单链表中的节点删除操作很方便,它可以直接改变指针指向来实现删除操作,而某些场景下数组的删除会导致移动剩下的元素。
  • 单链表中的元素访问需要通过顺序访问,即要通过遍历的方式来寻找元素,而数组则可以使用随机访问,这点算是单链表的缺点。

定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package main

// 节点的数据结构
type Node struct {
E int
Next *Node
}
// 链表的数据结构
type List struct {
dummyHead *Node
size int
}

func (l List) Head() *Node {
return l.dummyHead
}

func (l List) Size() int {
return l.size
}
// 初始化一个节点
func initNode (e int) *Node {
return &Node{
E:e,
Next:nil,
}
}
// 初始化一个空链表
func InitList () *List {
return &List{
dummyHead:initNode(0),
size:0,
}
}

虽然代码注释里写的是初始化一个空链表,却在里面实实在在地初始化了一个节点出来。这种技术叫做虚拟头结点,他的具体好处如下:

防止单链表是空的而设的,当链表为空的时候,带头结点的头指针就指向头结点。如果当链表为空的时候,单链表没有带头结点,那么它的头指针就为 NULL。

是为了方便单链表的特殊操作,插入在表头或者删除第一个结点。这样就保持了单链表操作的统一性。

单链表加上头结点之后,无论单链表是否为空,头指针始终指向头结点,因此空表和非空表的处理也统一了,方便了单链表的操作,也减少了程序的复杂性和出现 bug 的机会。

对单链表的多数操作应明确对哪个结点以及该结点的前驱。不带头结点的链表对首元结点、中间结点分别处理等;而带头结点的链表因为有头结点,首元结点、中间结点的操作相同,从而减少分支,使算法变得简单,流程清晰。

基本操作

  • 插入操作,其中又分为往一个索引对应的位置插入,往头结点后插入,在链表尾部插入三种插入方法。
  • 删除节点,其中又分为删除一个索引位置的节点,删除一个值对应的节点,删除第一个节点,删除尾节点。
  • 查询节点,同样分为查询某个索引值的节点,查询头部,查询尾部。
  • 修改节点,修改某个索引值对应的节点。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// 判断链表是否为空
func (this *List) IsEmpty() bool {
return this.size == 0
}
// 插入部分
// 在链表的第index索引个元素后插入元素,索引从0开始
func (this *List) AddIndex (index,e int) {
if index > this.size || index < 0 {
panic("索引越界,不能插入了")
}
prev := this.dummyHead
node := initNode(e)

for i:=0;i<index;i++ {
prev = prev.Next
}
// 这里的步骤一定不能反过来,思考一下为什么
node.Next = prev.Next
prev.Next = node
this.size++

}
// 在链表头添加元素
func (this *List) AddFirst (e int) {
this.AddIndex(0,e)
}
// 在链表尾部添加节点
func (this *List) AddLast (e int) {
this.AddIndex(this.size,e)
}
// 查询部分
// 在链表中查询是否包括元素e
func (this *List) Contains (e int) bool {
cur := this.dummyHead.Next
for cur!=nil {
if cur.E == e{
return true
}
cur = cur.Next
}
return false
}
// 在链表中查询第index个元素
func (this *List) Get (index int) int {
if index > this.size || index < 0 {
panic("索引越界,不能查询")
}
cur := this.dummyHead.Next
for i:=0;i<index;i++ {
cur = cur.Next
}
return cur.E
}
func (this *List) GetFirst () int{
return this.Get(0)
}
func (this *List) GetLast () int{
return this.Get(this.size-1)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
package main

import (
"fmt"
"strings"
)

type Node struct {
E int
Next *Node
}

type List struct {
dummyHead *Node
size int
}

func (l List) Head() *Node {
return l.dummyHead
}


func (l List) Size() int {
return l.size
}
func initNode (e int) *Node {
return &Node{
E:e,
Next:nil,
}
}
func InitList () *List {
return &List{
dummyHead:initNode(0),
size:0,
}
}
func (this *List) IsEmpty() bool {
return this.size == 0
}
// 在链表的第index索引个元素后插入元素,索引从0开始
func (this *List) AddIndex (index,e int) {
if index > this.size || index < 0 {
panic("索引越界,不能插入了")
}
prev := this.dummyHead
node := initNode(e)

for i:=0;i<index;i++ {
prev = prev.Next
}
node.Next = prev.Next
prev.Next = node
this.size++

}
// 在链表头添加元素
func (this *List) AddFirst (e int) {
this.AddIndex(0,e)
}
// 在链表尾部添加节点
func (this *List) AddLast (e int) {
this.AddIndex(this.size,e)
}
// 在链表中查询第index个元素
func (this *List) Get (index int) int {
if index > this.size || index < 0 {
panic("索引越界,不能查询")
}
cur := this.dummyHead.Next
for i:=0;i<index;i++ {
cur = cur.Next
}
return cur.E
}
func (this *List) GetFirst () int{
return this.Get(0)
}
func (this *List) GetLast () int{
return this.Get(this.size-1)
}
// 在链表index个位置中放入元素e
func (this *List) Set (index,e int) {
if index > this.size || index < 0 {
panic("索引越界,不能置入")
}
cur := this.dummyHead.Next
for i:=0;i<index;i++ {
cur = cur.Next
}
cur.E = e
}
// 在链表中查询是否包括元素e
func (this *List) Contains (e int) bool {
cur := this.dummyHead.Next
for cur!=nil {
if cur.E == e{
return true
}
cur = cur.Next
}
return false
}
// 在链表中删除元素
func (this *List) Remove (index int) int {
if index > this.size || index < 0 {
panic("索引越界,不能删除")
}
prev := this.dummyHead
for i:=0;i<index;i++ {
prev = prev.Next
}
retNode := prev.Next
prev.Next = retNode.Next
this.size--
return retNode.E
}
func (this *List) RemoveFirst () int{
return this.Remove(0)
}
func (this *List) RemoveLast () int{
return this.Remove(this.size-1)
}
// 删除元素E
func (this *List) RemoveElement (e int) {
prev := this.dummyHead
for prev.Next != nil {
if prev.E == e {
break
}
prev = prev.Next
}
if prev.Next != nil {
DelNode := prev.Next
prev.Next = DelNode.Next
DelNode = nil
}
}
// 在Golang中,如果我们想对自建数据结构自定义在Println的时候打印出什么结果
// 就可以使用这种方式去自己构建打印的字符串格式
func (this *List) String () string {
var builder strings.Builder
cur := this.dummyHead.Next
for cur != nil {
fmt.Fprintf(&builder,"%d -> ",cur.E)
cur = cur.Next
}
fmt.Fprintf(&builder,"NULL")
return builder.String()
}

func main() {
list := InitList()
for i:=0;i<5 ; i++ {
list.AddLast(i)
}
fmt.Println(list)
fmt.Println(list.Contains(0))
fmt.Println(list.Get(3))
fmt.Println(list.RemoveLast())
fmt.Println(list.GetFirst())
// 其他功能可以自行测试。
}

链表的应用

逆序一个链表

给定一个带头结点的单链表,请将其逆序。即如果单链表原来为 head -> 1 -> 2 -> 3 -> 4 -> 5 -> 6 -> 7 。则逆序后变为 head -> 7 -> 6 -> 5 -> 4- > 3 -> 2 -> 1。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package main

import (
"fmt"
"strings"
)

// 在这个函数前需要将list.go的代码拷贝到这里

func (this *List)Reverse() {
var pre *Node
var cur *Node
next := this.Head().Next

for next != nil {
cur = next.Next
next.Next = pre
pre = next
next = cur
}
this.Head().Next = pre
}

func main(){
list := InitList()
for i:=0;i<5 ; i++ {
list.AddFirst(i)
}
fmt.Println(list)
list.Reverse()
fmt.Println(list)
}

计算两个单链表所代表的的代数之和

给定两个单链表,链表的每个结点代表一位数, 计算两个数的和。例如 : 输入链表 (3->1->5) 和链表 (5->9->2) ,输出: 8->0->8 ,即 513 + 295 = 808 ,注意个位数在链表头。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
package main

import (
"fmt"
"strings"
)

// 在这个函数前需要将list.go的代码拷贝到这里


// 把两个用链表表示的整数相加
func AddTwoList(head1,head2 *Node) *List {
// 初始化一个新链表
newHead := InitList()
// 因为有dummyHead,所以head的下一个节点才是真真正的头结点
p1 := head1.Next
p2 := head2.Next
carry := 0
for p1 != nil || p2 != nil {
// 如果p1到头就只需要添加p2,反之同理
if p1 == nil {
newHead.AddLast(p2.E+ + carry)
p2 = p2.Next
}
if p2 == nil {
newHead.AddLast(p1.E + carry)
p1 = p1.Next
}
// 计算本位的值
temp := (p1.E + p2.E + carry )%10
newHead.AddLast(temp)
// 记录进位
carry = (p1.E + p2.E + carry )/10
p1 = p1.Next
p2 = p2.Next
}
return newHead
}

// 测试代码和测试结果
func main(){
list := InitList()
list2 := InitList()
for i:=0;i<6 ; i++ {
list.AddFirst(i)
}
for i:=0;i<6 ; i++ {
list2.AddFirst(i+1)
}
fmt.Println(list)
fmt.Println(list2)
newlist := AddTwoList(list.Head(),list2.Head())
fmt.Println(newlist)
}

忽忆故人今总老。贪梦好。茫然忘了邯郸道。

简述

队列是一种特殊的线性表,特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。

队列的先进先出,其实就对应了我们生活中的先到先得。比如你在食堂打饭,肯定是队头的人先打饭。还有在银行的叫号系统,那其实也是一个队列。

定义

队列肯定要装不只一个对象,所以我们需要一个切片作为容器。并且,我们还需要两个标记位,一个指向队列的头 front ,一个指向队列的尾 rear 。并且我们也需要一个队列的容量 size 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package main

type Queue struct {
// 用来装元素的切片
container []int
// 队首标记位
front int
// 队尾标记位
rear int
// 容量限制
size int
}

// 初始化时,要传入容量限制
func NewQueue(size int) *Queue {
return &Queue{
container: make([]int,size),
// 在队列中已经装了多少元素我们可以用size表示
// 想一想为什么不能用rear-front表示队列当前长度呢?
front: 0,
rear: 0,
size: 0,
}
}

队列的基本操作

队列的基本操作包括如下几种

  • 初始化队列:InitQueue(size) 操作结果:初始化一个空队列。
  • 判断队列是否为空:IsEmpty() 操作结果:若队列为空则返回 true,否则返回 false。
  • 判断队列是否已满:IsFull()。 操作结果:若队列为满则返回 true,否则返回 false。
  • 入队操作:EnQueue(data) 操作结果:在队列的队尾插入 data。
  • 出队操作:DeQueue() 操作结果:将队列的队头元素出队,并返回出队元素的值。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
package main

import "fmt"

type Queue struct {
// 用来装元素的切片
container []int
// 队首标记位
front int
// 队尾标记位
rear int
// 容量限制
size int
}

// 初始化时,要传入容量限制
func NewQueue(size int) *Queue {
return &Queue{
container: make([]int,size),
// 在队列中已经装了多少元素我们可以用size表示
// 想一想为什么不用 rear - front 表示呢
front: 0,
rear: 0,
size: 0,
}
}
// 队列是否为空
func (q *Queue) IsEmpty () bool {
if q.size == 0 {
return true
}
return false
}

func (q *Queue) IsFull () bool {
if q.size == len(q.container) {
return true
}
return false
}
// 新元素入队
func (q *Queue) EnQueue (a int) bool {
if q.IsFull() {
return false
}
q.container[q.rear] = a
q.rear++
q.size++
return true
}
// 队首元素出队
func (q *Queue) Dequeue () (bool,int) {
if q.IsEmpty() {
return false,0
}
ret := q.container[q.front]
q.front++
q.size--
return true,ret
}

func main() {
queue := NewQueue(10)

for i := 0; i < 5 ; i++ {
queue.EnQueue(i)
}

for i := 0; i < 6; i++ {
fmt.Println(queue.Dequeue())
}
}

循环队列

我们在每次让元素出队的时候,队列的头指针是不断的在往后移的。总有一天,队头指向的索引值比我们的容器长度还要大,那种情况出现的时候要怎么办呢?
既然在使用过程中,front 和 rear 都只能往后移导致有用的空间被浪费了。那么能不能去做一个可以再利用,不浪费一点空间的队列呢?

答案就是我们可以做一个循环队列。循环队列,顾名思义,它长得像一个环。原本数组是有头有尾的,是一条直线。现在我们把首尾相连,扳成了一个环。

判断队列的状态

用 size 表示队列的实际长度,而不用 rear - front 呢?其实就是为了循环队列。各位想一想,在循环时,rear 和 front 是说不清谁大谁小的,有可能减出一个负数。而我们使用 size 就不会出现这种问题,因为只有入队操作才会让 size + 1, 也只有出队操作能让 size - 1。所以这里我们的判断队列状态的函数可以直接不变就拿来使用。解决了循环队列中的最难点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/** 检查循环队列是否为空. */
func (this *Queue) IsEmpty() bool {
if this.size == 0 {
return true
}
return false
}

/** 检查循环队列是否为满 */
func (this *Queue) IsFull() bool {
if this.size == len(this.arr) {
return true
}
return false
}

计算索引

解决了判断状态问题,我们可以继续解决索引值的计算问题了。首先是入队操作,在入队时。我们只要对 rear 先对队列的容量取余,计算出的索引值就是我们要插入的位置。

对于出队操作,我们要出队的元素是 front 对队列容量取余所指向的元素,取出之后下一个 front 的值需要先对队列的容量取余再加 1。 到这里我们就可以写出新的队列的完整代码了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
package main

import (
"fmt"
)

type Queue struct {
container []int
front int
rear int
size int
}


func NewQueue (k int) *Queue {
return &Queue{
make([]int, k),
0,
0,
0,
}
}


func (this *Queue) EnQueue(value int) bool {
if this.container == nil || this.IsFull() {
return false
}
this.container[this.rear%len(this.container)] = value
this.rear = this.rear%len(this.container) + 1
this.size++
return true
}


func (this *Queue) DeQueue() (bool,int) {
if this.container == nil || this.IsEmpty() {
return false,0
} else {
ret := this.container[this.front%len(this.container)]
this.front = this.front%len(this.container) + 1
this.size--
return true,ret
}
}


func (this *Queue) IsEmpty() bool {
if this.size == 0 {
return true
}
return false
}

func (this *Queue) IsFull() bool {
if this.size == len(this.container) {
return true
}
return false
}

func main() {
queue := NewQueue(5)
// 循环3次,每次添加5个元素,再出队三个元素
for i:=0; i<3; i++{
for j:=0;j<5;j++ {
queue.EnQueue(j)
}
for k:=0;k<3;k++ {
fmt.Println(queue.DeQueue())
}
}
}

东风夜放花千树,更吹落,星如雨。

简述

栈是一种操作受限制的线性表,将允许进行插入、删除的一端称为栈顶,另一端称为栈底。看到这里你可能会觉得有点绕,其实数据结构很多的定义都很抽象,这是很正常的,下面我将类比一个生活中的常见实例帮助大家理解。

我们在日常生活中洗盘子的时候,摞起来的盘子就是一个典型的栈结构。不管取还是放,总是要放在盘子堆的最上面操作。如果你想从一堆盘子的中间强行取一个盘子,那就有可能酿成大祸。

定义

我们在日常生活中,一摞盘子肯定有很多个而且是连续的,所以我们首先需要一个切片,并且我们日常生活中的盘子是不能无限的摞的,这点在计算机里也是同样的,计算机里也会有 Stack Overflow 的问题。 所以我们还需要一个容量的限制元素。 并且我们还需要频繁的对栈顶元素进行操作,所以我们还需要一个标记位 top 来标记栈顶元素的索引。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package main

type Stack struct {
// 用来装元素的切片
container []int
// 栈顶标记位
top int
// 容量限制
size int
}

// 初始化时,要传入容量限制
func NewStack(size int) *Stack {
return &Stack{
container: make([]int,size),
// 栈顶指针初始可以指向-1,也可以指向0,想一想为什么。
top: 0,
size: size,
}
}

初始化时要注意,我们这里返回的是一个 Stack 的指针,引用传递在系统中的开销比较小

栈的基本操作

对于一个栈来说,其基本操作分为以下四种。

  • Push(e E) , 将一个数据类型为 E 的元素 e 放到栈顶。
  • Pop() , 将栈顶元素取出。
  • IsFul() , 栈是否满了。
  • IsEmpty() , 栈是否空了。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
package main

import "fmt"

type Stack struct {
// 用来装元素的切片
container []int
// 栈顶标记位
top int
// 容量限制
size int
}

// 初始化时,要传入容量限制
func NewStack(size int) Stack {
return Stack{
container: make([]int, size),
top: 0,
size: size,
}
}

func (s *Stack) Push(e int) bool {
if s.IsFull() {
return false
}
// 把盘子摞上去
s.container[s.top] = e
// 下一个能摞盘子的位置
s.top++
return true
}

func (s *Stack) Pop() (flag bool, ret int) {
// 如果栈空了,你就无法拿到新盘子,所以flag此时为false
if s.IsEmpty() {
return false, ret
}
// 取出盘子
ret = s.container[s.top-1]
// 下一个能取盘子的位置
s.top--
return true, ret
}

func (s *Stack) IsEmpty() bool {
if s.top == 0 {
return true
}
return false
}

func (s *Stack) IsFull() bool {
if s.top == s.size {
return true
}
return false
}

func main() {
stack := NewStack(3)
// 先测试栈为空的时候能否Pop
fmt.Println(stack.Pop())
// 测试Push是否正常
stack.Push(1)
stack.Push(2)
stack.Push(3)
// 如果栈为正常的,这里Pop打印顺序应该是3,2,1
fmt.Println(stack.Pop())
fmt.Println(stack.Pop())
fmt.Println(stack.Pop())
}

栈的常见应用

浏览器中的前进后退

假设你现在是 N 年前的 Chrome 浏览器工程师, 你现在很苦恼,有的网页在打开下一个页面后就回不去上一级了,你现在急迫的想要一个后退的功能,请问要怎么样实现呢?

仔细分析之后不难发现,所谓的后退,撤销等操作,其实就是一个栈的 Pop 操作,我们每次点击的网址, 或者进行的操作,都是被程序 Push 到了一个栈中,所以一旦我们点击撤销或后退时,总是可以返回我们最近一次的操作。这就是栈的最广泛的应用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
package main

import "fmt"

type Stack struct {
// 用来装元素的切片
container []string
// 栈顶标记位
top int
// 容量限制
size int
}

// 初始化时,要传入容量限制
func NewStack(size int) Stack {
return Stack{
container: make([]string, size),
top: 0,
size: size,
}
}

func (s *Stack) Push(e string) bool {
if s.IsFull() {
return false
}
s.container[s.top] = e
s.top++
return true
}

func (s *Stack) Pop() (flag bool, ret string) {
// 如果栈是空的,那么就不能继续 Pop 了
if s.IsEmpty() {
return false, ret
}
ret = s.container[s.top-1]
s.top--
return true, ret
}

func (s *Stack) IsEmpty() bool {
if s.top == 0 {
return true
}
return false
}

func (s *Stack) IsFull() bool {
if s.top == s.size {
return true
}
return false
}

func main() {
back := NewStack(3)
// 模拟每次点击网页时,浏览器会自push你的网址到栈中
back.Push("www.baidu.com")
back.Push("www.bing.com")
back.Push("www.goole.com")
// 每次点击后退时,就相当于是从栈中Pop了一个网址
fmt.Println(back.Pop())
fmt.Println(back.Pop())
fmt.Println(back.Pop())
}

括号匹配

在现代的 IDE 中,我们的编辑器环境是十分智能的,我少打了一个括号,IDE 就自动给我画了一条红线。是不是非常的神奇。其实这个功能也是用栈实现的。下面来说一下思路:
若遇到左括号入栈,遇到右括号时将栈顶左括号出栈,则遍历完所有括号后 stack 仍然为空;如果遍历之后 stack 不为空,那么说明有多余的左括号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
package main

import "fmt"

type Stack struct {
// 用来装元素的切片
container []byte
// 栈顶标记位
top int
// 容量限制
size int
}

// 初始化时,要传入容量限制
func NewStack(size int) Stack {
return Stack{
container: make([]byte,size),
top: 0,
size: size,
}
}

func (s *Stack) Push (e byte) bool {
if s.IsFull() {
return false
}
s.container[s.top] = e
s.top++
return true
}


func (s *Stack) Pop () (flag bool,ret byte) {
// 如果栈是空的,那么就不能继续 Pop 了
if s.IsEmpty() {
return false,ret
}
ret = s.container[s.top-1]
s.top--
return true,ret
}


func (s *Stack) IsEmpty () bool {
if s.top == 0 {
return true
}
return false
}


func (s *Stack) IsFull () bool {
if s.top == s.size {
return true
}
return false
}

func IsValid(s string) bool {
stack := NewStack(100)
// 遍历括号字符串
for _,v := range s {
if v == '(' {
// 由于golang中的字符串默认是unicode编码,所以我们要做一个强制类型转换
stack.Push(byte(v))
}
if v == ')' {
// 如果flag不为true,说明栈已经到底了,可以直返回false
if flag,top := stack.Pop(); flag == true &&top == '(' {
continue
} else {
return false
}
}
}
// 字符串遍历完后如果栈也空了,说明括号匹配
if stack.IsEmpty() {
return true
}
// 如果栈不空,说明栈里还有多余的左括号
return false
}

func main() {
test1 := "()()())"
test2 := "((()"
test3 := "()()()()"
fmt.Println(IsValid(test1))
fmt.Println(IsValid(test2))
fmt.Println(IsValid(test3))
}