日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁(yè) > 前端技术 > HTML >内容正文

HTML

前端-计算机基础

發(fā)布時(shí)間:2025/3/21 HTML 29 豆豆
生活随笔 收集整理的這篇文章主要介紹了 前端-计算机基础 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

一、網(wǎng)絡(luò)

#1 UDP

1.1 面向報(bào)文

UDP?是一個(gè)面向報(bào)文(報(bào)文可以理解為一段段的數(shù)據(jù))的協(xié)議。意思就是?UDP?只是報(bào)文的搬運(yùn)工,不會(huì)對(duì)報(bào)文進(jìn)行任何拆分和拼接操作

具體來說

  • 在發(fā)送端,應(yīng)用層將數(shù)據(jù)傳遞給傳輸層的?UDP?協(xié)議,UDP?只會(huì)給數(shù)據(jù)增加一個(gè)?UDP?頭標(biāo)識(shí)下是?UDP?協(xié)議,然后就傳遞給網(wǎng)絡(luò)層了
  • 在接收端,網(wǎng)絡(luò)層將數(shù)據(jù)傳遞給傳輸層,UDP?只去除?IP?報(bào)文頭就傳遞給應(yīng)用層,不會(huì)任何拼接操作

1.2 不可靠性

  • UDP?是無(wú)連接的,也就是說通信不需要建立和斷開連接。
  • UDP?也是不可靠的。協(xié)議收到什么數(shù)據(jù)就傳遞什么數(shù)據(jù),并且也不會(huì)備份數(shù)據(jù),對(duì)方能不能收到是不關(guān)心的
  • UDP?沒有擁塞控制,一直會(huì)以恒定的速度發(fā)送數(shù)據(jù)。即使網(wǎng)絡(luò)條件不好,也不會(huì)對(duì)發(fā)送速率進(jìn)行調(diào)整。這樣實(shí)現(xiàn)的弊端就是在網(wǎng)絡(luò)條件不好的情況下可能會(huì)導(dǎo)致丟包,但是優(yōu)點(diǎn)也很明顯,在某些實(shí)時(shí)性要求高的場(chǎng)景(比如電話會(huì)議)就需要使用 UDP 而不是?TCP

1.3 高效

  • 因?yàn)?UDP?沒有?TCP?那么復(fù)雜,需要保證數(shù)據(jù)不丟失且有序到達(dá)。所以?UDP?的頭部開銷小,只有八字節(jié),相比?TCP?的至少二十字節(jié)要少得多,在傳輸數(shù)據(jù)報(bào)文時(shí)是很高效的

頭部包含了以下幾個(gè)數(shù)據(jù)

  • 兩個(gè)十六位的端口號(hào),分別為源端口(可選字段)和目標(biāo)端口 整個(gè)數(shù)據(jù)報(bào)文的長(zhǎng)度
  • 整個(gè)數(shù)據(jù)報(bào)文的檢驗(yàn)和(IPv4?可選 字段),該字段用于發(fā)現(xiàn)頭部信息和數(shù)據(jù)中的錯(cuò)誤

1.4 傳輸方式

UDP?不止支持一對(duì)一的傳輸方式,同樣支持一對(duì)多,多對(duì)多,多對(duì)一的方式,也就是說 UDP 提供了單播,多播,廣播的功能

#2 TCP

2.1 頭部

TCP?頭部比?UDP?頭部復(fù)雜的多

對(duì)于?TCP?頭部來說,以下幾個(gè)字段是很重要的

  • Sequence number,這個(gè)序號(hào)保證了?TCP?傳輸?shù)膱?bào)文都是有序的,對(duì)端可以通過序號(hào)順序的拼接報(bào)文
  • Acknowledgement Number,這個(gè)序號(hào)表示數(shù)據(jù)接收端期望接收的下一個(gè)字節(jié)的編號(hào)是多少,同時(shí)也表示上一個(gè)序號(hào)的數(shù)據(jù)已經(jīng)收到
  • Window Size,窗口大小,表示還能接收多少字節(jié)的數(shù)據(jù),用于流量控制

標(biāo)識(shí)符

  • URG=1:該字段為一表示本數(shù)據(jù)報(bào)的數(shù)據(jù)部分包含緊急信息,是一個(gè)高優(yōu)先級(jí)數(shù)據(jù)報(bào)文,此時(shí)緊急指針有效。緊急數(shù)據(jù)一定位于當(dāng)前數(shù)據(jù)包數(shù)據(jù)部分的最前面,緊急指針標(biāo)明了緊急數(shù)據(jù)的尾部。
  • ACK=1:該字段為一表示確認(rèn)號(hào)字段有效。此外,TCP?還規(guī)定在連接建立后傳送的所有報(bào)文段都必須把?ACK?置為一?PSH=1:該字段為一表示接收端應(yīng)該立即將數(shù)據(jù) push 給應(yīng)用層,而不是等到緩沖區(qū)滿后再提交。
  • RST=1:該字段為一表示當(dāng)前?TCP?連接出現(xiàn)嚴(yán)重問題,可能需要重新建立?TCP?連接,也可以用于拒絕非法的報(bào)文段和拒絕連接請(qǐng)求。
  • SYN=1:當(dāng)SYN=1,ACK=0時(shí),表示當(dāng)前報(bào)文段是一個(gè)連接請(qǐng)求報(bào)文。當(dāng)SYN=1,ACK=1時(shí),表示當(dāng)前報(bào)文段是一個(gè)同意建立連接的應(yīng)答報(bào)文。
  • FIN=1:該字段為一表示此報(bào)文段是一個(gè)釋放連接的請(qǐng)求報(bào)文

2.2 狀態(tài)機(jī)

HTTP?是無(wú)連接的,所以作為下層的?TCP?協(xié)議也是無(wú)連接的,雖然看似?TCP?將兩端連接了起來,但是其實(shí)只是兩端共同維護(hù)了一個(gè)狀態(tài)

  • TCP?的狀態(tài)機(jī)是很復(fù)雜的,并且與建立斷開連接時(shí)的握手息息相關(guān),接下來就來詳細(xì)描述下兩種握手。
  • 在這之前需要了解一個(gè)重要的性能指標(biāo) RTT。該指標(biāo)表示發(fā)送端發(fā)送數(shù)據(jù)到接收到對(duì)端數(shù)據(jù)所需的往返時(shí)間

建立連接三次握手

  • 在?TCP?協(xié)議中,主動(dòng)發(fā)起請(qǐng)求的一端為客戶端,被動(dòng)連接的一端稱為服務(wù)端。不管是客戶端還是服務(wù)端,TCP連接建立完后都能發(fā)送和接收數(shù)據(jù),所以?TCP?也是一個(gè)全雙工的協(xié)議。
  • 起初,兩端都為?CLOSED?狀態(tài)。在通信開始前,雙方都會(huì)創(chuàng)建?TCB。 服務(wù)器創(chuàng)建完?TCB?后遍進(jìn)入?LISTEN?狀態(tài),此時(shí)開始等待客戶端發(fā)送數(shù)據(jù)

第一次握手

客戶端向服務(wù)端發(fā)送連接請(qǐng)求報(bào)文段。該報(bào)文段中包含自身的數(shù)據(jù)通訊初始序號(hào)。請(qǐng)求發(fā)送后,客戶端便進(jìn)入 SYN-SENT 狀態(tài),x 表示客戶端的數(shù)據(jù)通信初始序號(hào)。

第二次握手

服務(wù)端收到連接請(qǐng)求報(bào)文段后,如果同意連接,則會(huì)發(fā)送一個(gè)應(yīng)答,該應(yīng)答中也會(huì)包含自身的數(shù)據(jù)通訊初始序號(hào),發(fā)送完成后便進(jìn)入?SYN-RECEIVED?狀態(tài)。

第三次握手

當(dāng)客戶端收到連接同意的應(yīng)答后,還要向服務(wù)端發(fā)送一個(gè)確認(rèn)報(bào)文。客戶端發(fā)完這個(gè)報(bào)文段后便進(jìn)入ESTABLISHED?狀態(tài),服務(wù)端收到這個(gè)應(yīng)答后也進(jìn)入?ESTABLISHED狀態(tài),此時(shí)連接建立成功。

  • PS:第三次握手可以包含數(shù)據(jù),通過?TCP?快速打開(TFO)技術(shù)。其實(shí)只要涉及到握手的協(xié)議,都可以使用類似?TFO?的方式,客戶端和服務(wù)端存儲(chǔ)相同?cookie,下次握手時(shí)發(fā)出?cookie達(dá)到減少?RTT?的目的

你是否有疑惑明明兩次握手就可以建立起連接,為什么還需要第三次應(yīng)答?

  • 因?yàn)檫@是為了防止失效的連接請(qǐng)求報(bào)文段被服務(wù)端接收,從而產(chǎn)生錯(cuò)誤

可以想象如下場(chǎng)景。客戶端發(fā)送了一個(gè)連接請(qǐng)求 A,但是因?yàn)榫W(wǎng)絡(luò)原因造成了超時(shí),這時(shí) TCP 會(huì)啟動(dòng)超時(shí)重傳的機(jī)制再次發(fā)送一個(gè)連接請(qǐng)求 B。此時(shí)請(qǐng)求順利到達(dá)服務(wù)端,服務(wù)端應(yīng)答完就建立了請(qǐng)求。如果連接請(qǐng)求 A 在兩端關(guān)閉后終于抵達(dá)了服務(wù)端,那么這時(shí)服務(wù)端會(huì)認(rèn)為客戶端又需要建立 TCP 連接,從而應(yīng)答了該請(qǐng)求并進(jìn)入?ESTABLISHED?狀態(tài)。此時(shí)客戶端其實(shí)是 CLOSED 狀態(tài),那么就會(huì)導(dǎo)致服務(wù)端一直等待,造成資源的浪費(fèi)

PS:在建立連接中,任意一端掉線,TCP 都會(huì)重發(fā) SYN 包,一般會(huì)重試五次,在建立連接中可能會(huì)遇到 SYN FLOOD 攻擊。遇到這種情況你可以選擇調(diào)低重試次數(shù)或者干脆在不能處理的情況下拒絕請(qǐng)求

斷開鏈接四次握手

TCP?是全雙工的,在斷開連接時(shí)兩端都需要發(fā)送?FIN?和?ACK。

第一次握手

若客戶端 A 認(rèn)為數(shù)據(jù)發(fā)送完成,則它需要向服務(wù)端 B 發(fā)送連接釋放請(qǐng)求。

第二次握手

B 收到連接釋放請(qǐng)求后,會(huì)告訴應(yīng)用層要釋放 TCP 鏈接。然后會(huì)發(fā)送 ACK 包,并進(jìn)入 CLOSE_WAIT 狀態(tài),表示 A 到 B 的連接已經(jīng)釋放,不接收 A 發(fā)的數(shù)據(jù)了。但是因?yàn)?TCP 連接時(shí)雙向的,所以 B 仍舊可以發(fā)送數(shù)據(jù)給 A。

第三次握手

B 如果此時(shí)還有沒發(fā)完的數(shù)據(jù)會(huì)繼續(xù)發(fā)送,完畢后會(huì)向 A 發(fā)送連接釋放請(qǐng)求,然后 B 便進(jìn)入 LAST-ACK 狀態(tài)。

PS:通過延遲確認(rèn)的技術(shù)(通常有時(shí)間限制,否則對(duì)方會(huì)誤認(rèn)為需要重傳),可以將第二次和第三次握手合并,延遲 ACK 包的發(fā)送。

第四次握手

  • A 收到釋放請(qǐng)求后,向 B 發(fā)送確認(rèn)應(yīng)答,此時(shí) A 進(jìn)入 TIME-WAIT 狀態(tài)。該狀態(tài)會(huì)持續(xù) 2MSL(最大段生存期,指報(bào)文段在網(wǎng)絡(luò)中生存的時(shí)間,超時(shí)會(huì)被拋棄) 時(shí)間,若該時(shí)間段內(nèi)沒有 B 的重發(fā)請(qǐng)求的話,就進(jìn)入 CLOSED 狀態(tài)。當(dāng) B 收到確認(rèn)應(yīng)答后,也便進(jìn)入 CLOSED 狀態(tài)。

為什么 A 要進(jìn)入 TIME-WAIT 狀態(tài),等待 2MSL 時(shí)間后才進(jìn)入 CLOSED 狀態(tài)?

  • 為了保證 B 能收到 A 的確認(rèn)應(yīng)答。若 A 發(fā)完確認(rèn)應(yīng)答后直接進(jìn)入 CLOSED 狀態(tài),如果確認(rèn)應(yīng)答因?yàn)榫W(wǎng)絡(luò)問題一直沒有到達(dá),那么會(huì)造成 B 不能正常關(guān)閉

#3 HTTP

HTTP?協(xié)議是個(gè)無(wú)狀態(tài)協(xié)議,不會(huì)保存狀態(tài)

3.1 Post 和 Get 的區(qū)別

  • Get請(qǐng)求能緩存,Post?不能
  • Post?相對(duì)?Get安全一點(diǎn)點(diǎn),因?yàn)镚et?請(qǐng)求都包含在?URL?里,且會(huì)被瀏覽器保存歷史紀(jì)錄,Post?不會(huì),但是在抓包的情況下都是一樣的。
  • Post?可以通過?request body來傳輸比?Get?更多的數(shù)據(jù),Get沒有這個(gè)技術(shù)
  • URL有長(zhǎng)度限制,會(huì)影響?Get請(qǐng)求,但是這個(gè)長(zhǎng)度限制是瀏覽器規(guī)定的,不是?RFC?規(guī)定的
  • Post?支持更多的編碼類型且不對(duì)數(shù)據(jù)類型限制

3.2 常見狀態(tài)碼

2XX 成功

  • 200 OK,表示從客戶端發(fā)來的請(qǐng)求在服務(wù)器端被正確處理
  • 204 No content,表示請(qǐng)求成功,但響應(yīng)報(bào)文不含實(shí)體的主體部分
  • 205 Reset Content,表示請(qǐng)求成功,但響應(yīng)報(bào)文不含實(shí)體的主體部分,但是與?204?響應(yīng)不同在于要求請(qǐng)求方重置內(nèi)容
  • 206 Partial Content,進(jìn)行范圍請(qǐng)求

3XX 重定向

  • 301 moved permanently,永久性重定向,表示資源已被分配了新的 URL
  • 302 found,臨時(shí)性重定向,表示資源臨時(shí)被分配了新的 URL
  • 303 see other,表示資源存在著另一個(gè) URL,應(yīng)使用 GET 方法丁香獲取資源
  • 304 not modified,表示服務(wù)器允許訪問資源,但因發(fā)生請(qǐng)求未滿足條件的情況
  • 307 temporary redirect,臨時(shí)重定向,和302含義類似,但是期望客戶端保持請(qǐng)求方法不變向新的地址發(fā)出請(qǐng)求

4XX 客戶端錯(cuò)誤

  • 400 bad request,請(qǐng)求報(bào)文存在語(yǔ)法錯(cuò)誤
  • 401 unauthorized,表示發(fā)送的請(qǐng)求需要有通過?HTTP認(rèn)證的認(rèn)證信息
  • 403 forbidden,表示對(duì)請(qǐng)求資源的訪問被服務(wù)器拒絕
  • 404 not found,表示在服務(wù)器上沒有找到請(qǐng)求的資源

5XX 服務(wù)器錯(cuò)誤

  • 500 internal sever error,表示服務(wù)器端在執(zhí)行請(qǐng)求時(shí)發(fā)生了錯(cuò)誤
  • 501 Not Implemented,表示服務(wù)器不支持當(dāng)前請(qǐng)求所需要的某個(gè)功能
  • 503 service unavailable,表明服務(wù)器暫時(shí)處于超負(fù)載或正在停機(jī)維護(hù),無(wú)法處理請(qǐng)求

3.3 HTTP 首部

通用字段作用
Cache-Control控制緩存的行為
Connection瀏覽器想要優(yōu)先使用的連接類型,比如?keep-alive
Date創(chuàng)建報(bào)文時(shí)間
Pragma報(bào)文指令
Via代理服務(wù)器相關(guān)信息
Transfer-Encoding傳輸編碼方式
Upgrade要求客戶端升級(jí)協(xié)議
Warning在內(nèi)容中可能存在錯(cuò)誤
請(qǐng)求字段作用
Accept能正確接收的媒體類型
Accept-Charset能正確接收的字符集
Accept-Encoding能正確接收的編碼格式列表
Accept-Language能正確接收的語(yǔ)言列表
Expect期待服務(wù)端的指定行為
From請(qǐng)求方郵箱地址
Host服務(wù)器的域名
If-Match兩端資源標(biāo)記比較
If-Modified-Since本地資源未修改返回 304(比較時(shí)間)
If-None-Match本地資源未修改返回 304(比較標(biāo)記)
User-Agent客戶端信息
Max-Forwards限制可被代理及網(wǎng)關(guān)轉(zhuǎn)發(fā)的次數(shù)
Proxy-Authorization向代理服務(wù)器發(fā)送驗(yàn)證信息
Range請(qǐng)求某個(gè)內(nèi)容的一部分
Referer表示瀏覽器所訪問的前一個(gè)頁(yè)面
TE傳輸編碼方式
響應(yīng)字段作用
Accept-Ranges是否支持某些種類的范圍
Age資源在代理緩存中存在的時(shí)間
ETag資源標(biāo)識(shí)
Location客戶端重定向到某個(gè)?URL
Proxy-Authenticate向代理服務(wù)器發(fā)送驗(yàn)證信息
Server服務(wù)器名字
WWW-Authenticate獲取資源需要的驗(yàn)證信息
實(shí)體字段作用
Allow資源的正確請(qǐng)求方式
Content-Encoding內(nèi)容的編碼格式
Content-Language內(nèi)容使用的語(yǔ)言
Content-Lengthrequest body?長(zhǎng)度
Content-Location返回?cái)?shù)據(jù)的備用地址
Content-MD5Base64加密格式的內(nèi)容MD5檢驗(yàn)值
Content-Range內(nèi)容的位置范圍
Content-Type內(nèi)容的媒體類型
Expires內(nèi)容的過期時(shí)間
Last_modified內(nèi)容的最后修改時(shí)間

#4 DNS

DNS 的作用就是通過域名查詢到具體的 IP。

  • 因?yàn)?IP 存在數(shù)字和英文的組合(IPv6),很不利于人類記憶,所以就出現(xiàn)了域名。你可以把域名看成是某個(gè) IP 的別名,DNS 就是去查詢這個(gè)別名的真正名稱是什么

在?TCP?握手之前就已經(jīng)進(jìn)行了?DNS?查詢,這個(gè)查詢是操作系統(tǒng)自己做的。當(dāng)你在瀏覽器中想訪問?www.google.com?時(shí),會(huì)進(jìn)行一下操作

  • 操作系統(tǒng)會(huì)首先在本地緩存中查詢
  • 沒有的話會(huì)去系統(tǒng)配置的 DNS 服務(wù)器中查詢
  • 如果這時(shí)候還沒得話,會(huì)直接去 DNS 根服務(wù)器查詢,這一步查詢會(huì)找出負(fù)責(zé) com 這個(gè)一級(jí)域名的服務(wù)器
  • 然后去該服務(wù)器查詢 google 這個(gè)二級(jí)域名
  • 接下來三級(jí)域名的查詢其實(shí)是我們配置的,你可以給 www 這個(gè)域名配置一個(gè) IP,然后還可以給別的三級(jí)域名配置一個(gè) IP

以上介紹的是 DNS 迭代查詢,還有種是遞歸查詢,區(qū)別就是前者是由客戶端去做請(qǐng)求,后者是由系統(tǒng)配置的 DNS 服務(wù)器做請(qǐng)求,得到結(jié)果后將數(shù)據(jù)返回給客戶端。

#二、數(shù)據(jù)結(jié)構(gòu)

#2.1 棧

概念

  • 棧是一個(gè)線性結(jié)構(gòu),在計(jì)算機(jī)中是一個(gè)相當(dāng)常見的數(shù)據(jù)結(jié)構(gòu)。
  • 棧的特點(diǎn)是只能在某一端添加或刪除數(shù)據(jù),遵循先進(jìn)后出的原則

實(shí)現(xiàn)

每種數(shù)據(jù)結(jié)構(gòu)都可以用很多種方式來實(shí)現(xiàn),其實(shí)可以把棧看成是數(shù)組的一個(gè)子集,所以這里使用數(shù)組來實(shí)現(xiàn)

class Stack {constructor() {this.stack = []}push(item) {this.stack.push(item)}pop() {this.stack.pop()}peek() {return this.stack[this.getCount() - 1]}getCount() {return this.stack.length}isEmpty() {return this.getCount() === 0} }

應(yīng)用

匹配括號(hào),可以通過棧的特性來完成

var isValid = function (s) {let map = {'(': -1,')': 1,'[': -2,']': 2,'{': -3,'}': 3}let stack = []for (let i = 0; i < s.length; i++) {if (map[s[i]] < 0) {stack.push(s[i])} else {let last = stack.pop()if (map[last] + map[s[i]] != 0) return false}}if (stack.length > 0) return falsereturn true };

#2.2 隊(duì)列

概念

隊(duì)列一個(gè)線性結(jié)構(gòu),特點(diǎn)是在某一端添加數(shù)據(jù),在另一端刪除數(shù)據(jù),遵循先進(jìn)先出的原則

實(shí)現(xiàn)

這里會(huì)講解兩種實(shí)現(xiàn)隊(duì)列的方式,分別是單鏈隊(duì)列和循環(huán)隊(duì)列

  • 單鏈隊(duì)列
class Queue {constructor() {this.queue = []}enQueue(item) {this.queue.push(item)}deQueue() {return this.queue.shift()}getHeader() {return this.queue[0]}getLength() {return this.queue.length}isEmpty() {return this.getLength() === 0} }

因?yàn)閱捂滉?duì)列在出隊(duì)操作的時(shí)候需要?O(n)?的時(shí)間復(fù)雜度,所以引入了循環(huán)隊(duì)列。循環(huán)隊(duì)列的出隊(duì)操作平均是?O(1)?的時(shí)間復(fù)雜度

  • 循環(huán)隊(duì)列
class SqQueue {constructor(length) {this.queue = new Array(length + 1)// 隊(duì)頭this.first = 0// 隊(duì)尾this.last = 0// 當(dāng)前隊(duì)列大小this.size = 0}enQueue(item) {// 判斷隊(duì)尾 + 1 是否為隊(duì)頭// 如果是就代表需要擴(kuò)容數(shù)組// % this.queue.length 是為了防止數(shù)組越界if (this.first === (this.last + 1) % this.queue.length) {this.resize(this.getLength() * 2 + 1)}this.queue[this.last] = itemthis.size++this.last = (this.last + 1) % this.queue.length}deQueue() {if (this.isEmpty()) {throw Error('Queue is empty')}let r = this.queue[this.first]this.queue[this.first] = nullthis.first = (this.first + 1) % this.queue.lengththis.size--// 判斷當(dāng)前隊(duì)列大小是否過小// 為了保證不浪費(fèi)空間,在隊(duì)列空間等于總長(zhǎng)度四分之一時(shí)// 且不為 2 時(shí)縮小總長(zhǎng)度為當(dāng)前的一半if (this.size === this.getLength() / 4 && this.getLength() / 2 !== 0) {this.resize(this.getLength() / 2)}return r}getHeader() {if (this.isEmpty()) {throw Error('Queue is empty')}return this.queue[this.first]}getLength() {return this.queue.length - 1}isEmpty() {return this.first === this.last}resize(length) {let q = new Array(length)for (let i = 0; i < length; i++) {q[i] = this.queue[(i + this.first) % this.queue.length]}this.queue = qthis.first = 0this.last = this.size} }

#2.3 鏈表

概念

鏈表是一個(gè)線性結(jié)構(gòu),同時(shí)也是一個(gè)天然的遞歸結(jié)構(gòu)。鏈表結(jié)構(gòu)可以充分利用計(jì)算機(jī)內(nèi)存空間,實(shí)現(xiàn)靈活的內(nèi)存動(dòng)態(tài)管理。但是鏈表失去了數(shù)組隨機(jī)讀取的優(yōu)點(diǎn),同時(shí)鏈表由于增加了結(jié)點(diǎn)的指針域,空間開銷比較大

實(shí)現(xiàn)

  • 單向鏈表
class Node {constructor(v, next) {this.value = vthis.next = next} } class LinkList {constructor() {// 鏈表長(zhǎng)度this.size = 0// 虛擬頭部this.dummyNode = new Node(null, null)}find(header, index, currentIndex) {if (index === currentIndex) return headerreturn this.find(header.next, index, currentIndex + 1)}addNode(v, index) {this.checkIndex(index)// 當(dāng)往鏈表末尾插入時(shí),prev.next 為空// 其他情況時(shí),因?yàn)橐迦牍?jié)點(diǎn),所以插入的節(jié)點(diǎn)// 的 next 應(yīng)該是 prev.next// 然后設(shè)置 prev.next 為插入的節(jié)點(diǎn)let prev = this.find(this.dummyNode, index, 0)prev.next = new Node(v, prev.next)this.size++return prev.next}insertNode(v, index) {return this.addNode(v, index)}addToFirst(v) {return this.addNode(v, 0)}addToLast(v) {return this.addNode(v, this.size)}removeNode(index, isLast) {this.checkIndex(index)index = isLast ? index - 1 : indexlet prev = this.find(this.dummyNode, index, 0)let node = prev.nextprev.next = node.nextnode.next = nullthis.size--return node}removeFirstNode() {return this.removeNode(0)}removeLastNode() {return this.removeNode(this.size, true)}checkIndex(index) {if (index < 0 || index > this.size) throw Error('Index error')}getNode(index) {this.checkIndex(index)if (this.isEmpty()) returnreturn this.find(this.dummyNode, index, 0).next}isEmpty() {return this.size === 0}getSize() {return this.size} }

#2.4 樹

二叉樹

  • 樹擁有很多種結(jié)構(gòu),二叉樹是樹中最常用的結(jié)構(gòu),同時(shí)也是一個(gè)天然的遞歸結(jié)構(gòu)。
  • 二叉樹擁有一個(gè)根節(jié)點(diǎn),每個(gè)節(jié)點(diǎn)至多擁有兩個(gè)子節(jié)點(diǎn),分別為:左節(jié)點(diǎn)和右節(jié)點(diǎn)。樹的最底部節(jié)點(diǎn)稱之為葉節(jié)點(diǎn),當(dāng)一顆樹的葉數(shù)量數(shù)量為滿時(shí),該樹可以稱之為滿二叉樹

二分搜索樹

  • 二分搜索樹也是二叉樹,擁有二叉樹的特性。但是區(qū)別在于二分搜索樹每個(gè)節(jié)點(diǎn)的值都比他的左子樹的值大,比右子樹的值小
  • 這種存儲(chǔ)方式很適合于數(shù)據(jù)搜索。如下圖所示,當(dāng)需要查找 6 的時(shí)候,因?yàn)樾枰檎业闹当雀?jié)點(diǎn)的值大,所以只需要在根節(jié)點(diǎn)的右子樹上尋找,大大提高了搜索效率

  • 實(shí)現(xiàn)
class Node {constructor(value) {this.value = valuethis.left = nullthis.right = null} } class BST {constructor() {this.root = nullthis.size = 0}getSize() {return this.size}isEmpty() {return this.size === 0}addNode(v) {this.root = this._addChild(this.root, v)}// 添加節(jié)點(diǎn)時(shí),需要比較添加的節(jié)點(diǎn)值和當(dāng)前// 節(jié)點(diǎn)值的大小_addChild(node, v) {if (!node) {this.size++return new Node(v)}if (node.value > v) {node.left = this._addChild(node.left, v)} else if (node.value < v) {node.right = this._addChild(node.right, v)}return node} }
  • 以上是最基本的二分搜索樹實(shí)現(xiàn),接下來實(shí)現(xiàn)樹的遍歷。

對(duì)于樹的遍歷來說,有三種遍歷方法,分別是先序遍歷、中序遍歷、后序遍歷。三種遍歷的區(qū)別在于何時(shí)訪問節(jié)點(diǎn)。在遍歷樹的過程中,每個(gè)節(jié)點(diǎn)都會(huì)遍歷三次,分別是遍歷到自己,遍歷左子樹和遍歷右子樹。如果需要實(shí)現(xiàn)先序遍歷,那么只需要第一次遍歷到節(jié)點(diǎn)時(shí)進(jìn)行操作即可

// 先序遍歷可用于打印樹的結(jié)構(gòu) // 先序遍歷先訪問根節(jié)點(diǎn),然后訪問左節(jié)點(diǎn),最后訪問右節(jié)點(diǎn)。 preTraversal() {this._pre(this.root) } _pre(node) {if (node) {console.log(node.value)this._pre(node.left)this._pre(node.right)} } // 中序遍歷可用于排序 // 對(duì)于 BST 來說,中序遍歷可以實(shí)現(xiàn)一次遍歷就 // 得到有序的值 // 中序遍歷表示先訪問左節(jié)點(diǎn),然后訪問根節(jié)點(diǎn),最后訪問右節(jié)點(diǎn)。 midTraversal() {this._mid(this.root) } _mid(node) {if (node) {this._mid(node.left)console.log(node.value)this._mid(node.right)} } // 后序遍歷可用于先操作子節(jié)點(diǎn) // 再操作父節(jié)點(diǎn)的場(chǎng)景 // 后序遍歷表示先訪問左節(jié)點(diǎn),然后訪問右節(jié)點(diǎn),最后訪問根節(jié)點(diǎn)。 backTraversal() {this._back(this.root) } _back(node) {if (node) {this._back(node.left)this._back(node.right)console.log(node.value)} }

以上的這幾種遍歷都可以稱之為深度遍歷,對(duì)應(yīng)的還有種遍歷叫做廣度遍歷,也就是一層層地遍歷樹。對(duì)于廣度遍歷來說,我們需要利用之前講過的隊(duì)列結(jié)構(gòu)來完成

breadthTraversal() {if (!this.root) return nulllet q = new Queue()// 將根節(jié)點(diǎn)入隊(duì)q.enQueue(this.root)// 循環(huán)判斷隊(duì)列是否為空,為空// 代表樹遍歷完畢while (!q.isEmpty()) {// 將隊(duì)首出隊(duì),判斷是否有左右子樹// 有的話,就先左后右入隊(duì)let n = q.deQueue()console.log(n.value)if (n.left) q.enQueue(n.left)if (n.right) q.enQueue(n.right)} }

接下來先介紹如何在樹中尋找最小值或最大數(shù)。因?yàn)槎炙阉鳂涞奶匦?#xff0c;所以最小值一定在根節(jié)點(diǎn)的最左邊,最大值相反

getMin() {return this._getMin(this.root).value } _getMin(node) {if (!node.left) return nodereturn this._getMin(node.left) } getMax() {return this._getMax(this.root).value } _getMax(node) {if (!node.right) return nodereturn this._getMin(node.right) }

向上取整和向下取整,這兩個(gè)操作是相反的,所以代碼也是類似的,這里只介紹如何向下取整。既然是向下取整,那么根據(jù)二分搜索樹的特性,值一定在根節(jié)點(diǎn)的左側(cè)。只需要一直遍歷左子樹直到當(dāng)前節(jié)點(diǎn)的值不再大于等于需要的值,然后判斷節(jié)點(diǎn)是否還擁有右子樹。如果有的話,繼續(xù)上面的遞歸判斷

floor(v) {let node = this._floor(this.root, v)return node ? node.value : null } _floor(node, v) {if (!node) return nullif (node.value === v) return v// 如果當(dāng)前節(jié)點(diǎn)值還比需要的值大,就繼續(xù)遞歸if (node.value > v) {return this._floor(node.left, v)}// 判斷當(dāng)前節(jié)點(diǎn)是否擁有右子樹let right = this._floor(node.right, v)if (right) return rightreturn node }

排名,這是用于獲取給定值的排名或者排名第幾的節(jié)點(diǎn)的值,這兩個(gè)操作也是相反的,所以這個(gè)只介紹如何獲取排名第幾的節(jié)點(diǎn)的值。對(duì)于這個(gè)操作而言,我們需要略微的改造點(diǎn)代碼,讓每個(gè)節(jié)點(diǎn)擁有一個(gè) size 屬性。該屬性表示該節(jié)點(diǎn)下有多少子節(jié)點(diǎn)(包含自身)

class Node {constructor(value) {this.value = valuethis.left = nullthis.right = null// 修改代碼this.size = 1} } // 新增代碼 _getSize(node) {return node ? node.size : 0 } _addChild(node, v) {if (!node) {return new Node(v)}if (node.value > v) {// 修改代碼node.size++node.left = this._addChild(node.left, v)} else if (node.value < v) {// 修改代碼node.size++node.right = this._addChild(node.right, v)}return node } select(k) {let node = this._select(this.root, k)return node ? node.value : null } _select(node, k) {if (!node) return null// 先獲取左子樹下有幾個(gè)節(jié)點(diǎn)let size = node.left ? node.left.size : 0// 判斷 size 是否大于 k// 如果大于 k,代表所需要的節(jié)點(diǎn)在左節(jié)點(diǎn)if (size > k) return this._select(node.left, k)// 如果小于 k,代表所需要的節(jié)點(diǎn)在右節(jié)點(diǎn)// 注意這里需要重新計(jì)算 k,減去根節(jié)點(diǎn)除了右子樹的節(jié)點(diǎn)數(shù)量if (size < k) return this._select(node.right, k - size - 1)return node }

接下來講解的是二分搜索樹中最難實(shí)現(xiàn)的部分:刪除節(jié)點(diǎn)。因?yàn)閷?duì)于刪除節(jié)點(diǎn)來說,會(huì)存在以下幾種情況

  • 需要?jiǎng)h除的節(jié)點(diǎn)沒有子樹
  • 需要?jiǎng)h除的節(jié)點(diǎn)只有一條子樹
  • 需要?jiǎng)h除的節(jié)點(diǎn)有左右兩條樹
  • 對(duì)于前兩種情況很好解決,但是第三種情況就有難度了,所以先來實(shí)現(xiàn)相對(duì)簡(jiǎn)單的操作:刪除最小節(jié)點(diǎn),對(duì)于刪除最小節(jié)點(diǎn)來說,是不存在第三種情況的,刪除最大節(jié)點(diǎn)操作是和刪除最小節(jié)點(diǎn)相反的,所以這里也就不再贅述
delectMin() {this.root = this._delectMin(this.root)console.log(this.root) } _delectMin(node) {// 一直遞歸左子樹// 如果左子樹為空,就判斷節(jié)點(diǎn)是否擁有右子樹// 有右子樹的話就把需要?jiǎng)h除的節(jié)點(diǎn)替換為右子樹if ((node != null) & !node.left) return node.rightnode.left = this._delectMin(node.left)// 最后需要重新維護(hù)下節(jié)點(diǎn)的 `size`node.size = this._getSize(node.left) + this._getSize(node.right) + 1return node }
  • 最后講解的就是如何刪除任意節(jié)點(diǎn)了。對(duì)于這個(gè)操作,T.Hibbard?在?1962年提出了解決這個(gè)難題的辦法,也就是如何解決第三種情況。
  • 當(dāng)遇到這種情況時(shí),需要取出當(dāng)前節(jié)點(diǎn)的后繼節(jié)點(diǎn)(也就是當(dāng)前節(jié)點(diǎn)右子樹的最小節(jié)點(diǎn))來替換需要?jiǎng)h除的節(jié)點(diǎn)。然后將需要?jiǎng)h除節(jié)點(diǎn)的左子樹賦值給后繼結(jié)點(diǎn),右子樹刪除后繼結(jié)點(diǎn)后賦值給他。
  • 你如果對(duì)于這個(gè)解決辦法有疑問的話,可以這樣考慮。因?yàn)槎炙阉鳂涞奶匦?#xff0c;父節(jié)點(diǎn)一定比所有左子節(jié)點(diǎn)大,比所有右子節(jié)點(diǎn)小。那么當(dāng)需要?jiǎng)h除父節(jié)點(diǎn)時(shí),勢(shì)必需要拿出一個(gè)比父節(jié)點(diǎn)大的節(jié)點(diǎn)來替換父節(jié)點(diǎn)。這個(gè)節(jié)點(diǎn)肯定不存在于左子樹,必然存在于右子樹。然后又需要保持父節(jié)點(diǎn)都是比右子節(jié)點(diǎn)小的,那么就可以取出右子樹中最小的那個(gè)節(jié)點(diǎn)來替換父節(jié)點(diǎn)
delect(v) {this.root = this._delect(this.root, v) } _delect(node, v) {if (!node) return null// 尋找的節(jié)點(diǎn)比當(dāng)前節(jié)點(diǎn)小,去左子樹找if (node.value < v) {node.right = this._delect(node.right, v)} else if (node.value > v) {// 尋找的節(jié)點(diǎn)比當(dāng)前節(jié)點(diǎn)大,去右子樹找node.left = this._delect(node.left, v)} else {// 進(jìn)入這個(gè)條件說明已經(jīng)找到節(jié)點(diǎn)// 先判斷節(jié)點(diǎn)是否擁有擁有左右子樹中的一個(gè)// 是的話,將子樹返回出去,這里和 `_delectMin` 的操作一樣if (!node.left) return node.rightif (!node.right) return node.left// 進(jìn)入這里,代表節(jié)點(diǎn)擁有左右子樹// 先取出當(dāng)前節(jié)點(diǎn)的后繼結(jié)點(diǎn),也就是取當(dāng)前節(jié)點(diǎn)右子樹的最小值let min = this._getMin(node.right)// 取出最小值后,刪除最小值// 然后把刪除節(jié)點(diǎn)后的子樹賦值給最小值節(jié)點(diǎn)min.right = this._delectMin(node.right)// 左子樹不動(dòng)min.left = node.leftnode = min}// 維護(hù) sizenode.size = this._getSize(node.left) + this._getSize(node.right) + 1return node }

#2.5 堆

概念

  • 堆通常是一個(gè)可以被看做一棵樹的數(shù)組對(duì)象。
  • 堆的實(shí)現(xiàn)通過構(gòu)造二叉堆,實(shí)為二叉樹的一種。這種數(shù)據(jù)結(jié)構(gòu)具有以下性質(zhì)。
  • 任意節(jié)點(diǎn)小于(或大于)它的所有子節(jié)點(diǎn) 堆總是一棵完全樹。即除了最底層,其他層的節(jié)點(diǎn)都被元素填滿,且最底層從左到右填入。
  • 將根節(jié)點(diǎn)最大的堆叫做最大堆或大根堆,根節(jié)點(diǎn)最小的堆叫做最小堆或小根堆。
  • 優(yōu)先隊(duì)列也完全可以用堆來實(shí)現(xiàn),操作是一模一樣的。

實(shí)現(xiàn)大根堆

堆的每個(gè)節(jié)點(diǎn)的左邊子節(jié)點(diǎn)索引是?i * 2 + 1,右邊是?i * 2 + 2,父節(jié)點(diǎn)是?(i - 1) /2。

  • 堆有兩個(gè)核心的操作,分別是?shiftUp?和?shiftDown?。前者用于添加元素,后者用于刪除根節(jié)點(diǎn)。
  • shiftUp?的核心思路是一路將節(jié)點(diǎn)與父節(jié)點(diǎn)對(duì)比大小,如果比父節(jié)點(diǎn)大,就和父節(jié)點(diǎn)交換位置。
  • shiftDown?的核心思路是先將根節(jié)點(diǎn)和末尾交換位置,然后移除末尾元素。接下來循環(huán)判斷父節(jié)點(diǎn)和兩個(gè)子節(jié)點(diǎn)的大小,如果子節(jié)點(diǎn)大,就把最大的子節(jié)點(diǎn)和父節(jié)點(diǎn)交換

class MaxHeap {constructor() {this.heap = []}size() {return this.heap.length}empty() {return this.size() == 0}add(item) {this.heap.push(item)this._shiftUp(this.size() - 1)}removeMax() {this._shiftDown(0)}getParentIndex(k) {return parseInt((k - 1) / 2)}getLeftIndex(k) {return k * 2 + 1}_shiftUp(k) {// 如果當(dāng)前節(jié)點(diǎn)比父節(jié)點(diǎn)大,就交換while (this.heap[k] > this.heap[this.getParentIndex(k)]) {this._swap(k, this.getParentIndex(k))// 將索引變成父節(jié)點(diǎn)k = this.getParentIndex(k)}}_shiftDown(k) {// 交換首位并刪除末尾this._swap(k, this.size() - 1)this.heap.splice(this.size() - 1, 1)// 判斷節(jié)點(diǎn)是否有左孩子,因?yàn)槎娑训奶匦?#xff0c;有右必有左while (this.getLeftIndex(k) < this.size()) {let j = this.getLeftIndex(k)// 判斷是否有右孩子,并且右孩子是否大于左孩子if (j + 1 < this.size() && this.heap[j + 1] > this.heap[j]) j++// 判斷父節(jié)點(diǎn)是否已經(jīng)比子節(jié)點(diǎn)都大if (this.heap[k] >= this.heap[j]) breakthis._swap(k, j)k = j}}_swap(left, right) {let rightValue = this.heap[right]this.heap[right] = this.heap[left]this.heap[left] = rightValue} }

#三、算法

#3.1 時(shí)間復(fù)雜度

  • 通常使用最差的時(shí)間復(fù)雜度來衡量一個(gè)算法的好壞。
  • 常數(shù)時(shí)間?O(1)?代表這個(gè)操作和數(shù)據(jù)量沒關(guān)系,是一個(gè)固定時(shí)間的操作,比如說四則運(yùn)算。
  • 對(duì)于一個(gè)算法來說,可能會(huì)計(jì)算出如下操作次數(shù)?aN +1,N?代表數(shù)據(jù)量。那么該算法的時(shí)間復(fù)雜度就是?O(N)。因?yàn)槲覀冊(cè)谟?jì)算時(shí)間復(fù)雜度的時(shí)候,數(shù)據(jù)量通常是非常大的,這時(shí)候低階項(xiàng)和常數(shù)項(xiàng)可以忽略不計(jì)。
  • 當(dāng)然可能會(huì)出現(xiàn)兩個(gè)算法都是?O(N)?的時(shí)間復(fù)雜度,那么對(duì)比兩個(gè)算法的好壞就要通過對(duì)比低階項(xiàng)和常數(shù)項(xiàng)了

#3.2 位運(yùn)算

  • 位運(yùn)算在算法中很有用,速度可以比四則運(yùn)算快很多。
  • 在學(xué)習(xí)位運(yùn)算之前應(yīng)該知道十進(jìn)制如何轉(zhuǎn)二進(jìn)制,二進(jìn)制如何轉(zhuǎn)十進(jìn)制。這里說明下簡(jiǎn)單的計(jì)算方式
  • 十進(jìn)制?33?可以看成是?32 + 1?,并且?33?應(yīng)該是六位二進(jìn)制的(因?yàn)?33近似?32,而?32?是?2的五次方,所以是六位),那么 十進(jìn)制?33?就是?100001?,只要是 2 的次方,那么就是?1否則都為?0?那么二進(jìn)制?100001?同理,首位是?2^5,末位是?2^0?,相加得出?33

左移 <<

10 << 1 // -> 20

左移就是將二進(jìn)制全部往左移動(dòng),10在二進(jìn)制中表示為?1010?,左移一位后變成?10100?,轉(zhuǎn)換為十進(jìn)制也就是?20,所以基本可以把左移看成以下公式?a * (2 ^ b)

算數(shù)右移 >>

10 >> 1 // -> 5
  • 算數(shù)右移就是將二進(jìn)制全部往右移動(dòng)并去除多余的右邊,10 在二進(jìn)制中表示為?1010?,右移一位后變成?101?,轉(zhuǎn)換為十進(jìn)制也就是?5,所以基本可以把右移看成以下公式?int v = a / (2 ^ b)
  • 右移很好用,比如可以用在二分算法中取中間值
13 >> 1 // -> 6

按位操作

  • 按位與

每一位都為 1,結(jié)果才為 1

8 & 7 // -> 0 // 1000 & 0111 -> 0000 -> 0
  • 按位或

其中一位為 1,結(jié)果就是 1

8 | 7 // -> 15 // 1000 | 0111 -> 1111 -> 15
  • 按位異或

每一位都不同,結(jié)果才為 1

8 ^ 7 // -> 15 8 ^ 8 // -> 0 // 1000 ^ 0111 -> 1111 -> 15 // 1000 ^ 1000 -> 0000 -> 0

面試題:兩個(gè)數(shù)不使用四則運(yùn)算得出和

這道題中可以按位異或,因?yàn)榘次划惢蚓褪遣贿M(jìn)位加法,8 ^ 8 = 0?如果進(jìn)位了,就是?16?了,所以我們只需要將兩個(gè)數(shù)進(jìn)行異或操作,然后進(jìn)位。那么也就是說兩個(gè)二進(jìn)制都是 1 的位置,左邊應(yīng)該有一個(gè)進(jìn)位?1,所以可以得出以下公式?a + b = (a ^ b) + ((a & b) << 1)?,然后通過迭代的方式模擬加法

function sum(a, b) {if (a == 0) return bif (b == 0) return alet newA = a ^ blet newB = (a & b) << 1return sum(newA, newB) }

#3.3 排序

冒泡排序

冒泡排序的原理如下,從第一個(gè)元素開始,把當(dāng)前元素和下一個(gè)索引元素進(jìn)行比較。如果當(dāng)前元素大,那么就交換位置,重復(fù)操作直到比較到最后一個(gè)元素,那么此時(shí)最后一個(gè)元素就是該數(shù)組中最大的數(shù)。下一輪重復(fù)以上操作,但是此時(shí)最后一個(gè)元素已經(jīng)是最大數(shù)了,所以不需要再比較最后一個(gè)元素,只需要比較到?length - 1?的位置

以下是實(shí)現(xiàn)該算法的代碼

function bubble(array) {checkArray(array);for (let i = array.length - 1; i > 0; i--) {// 從 0 到 `length - 1` 遍歷for (let j = 0; j < i; j++) {if (array[j] > array[j + 1]) swap(array, j, j + 1)}}return array; }

該算法的操作次數(shù)是一個(gè)等差數(shù)列?n + (n - 1) + (n - 2) + 1?,去掉常數(shù)項(xiàng)以后得出時(shí)間復(fù)雜度是O(n * n)

插入排序

入排序的原理如下。第一個(gè)元素默認(rèn)是已排序元素,取出下一個(gè)元素和當(dāng)前元素比較,如果當(dāng)前元素大就交換位置。那么此時(shí)第一個(gè)元素就是當(dāng)前的最小數(shù),所以下次取出操作從第三個(gè)元素開始,向前對(duì)比,重復(fù)之前的操作

以下是實(shí)現(xiàn)該算法的代碼

function insertion(array) {checkArray(array);for (let i = 1; i < array.length; i++) {for (let j = i - 1; j >= 0 && array[j] > array[j + 1]; j--)swap(array, j, j + 1);}return array; }

該算法的操作次數(shù)是一個(gè)等差數(shù)列?n + (n - 1) + (n - 2) + 1?,去掉常數(shù)項(xiàng)以后得出時(shí)間復(fù)雜度是?O(n * n)

選擇排序

選擇排序的原理如下。遍歷數(shù)組,設(shè)置最小值的索引為 0,如果取出的值比當(dāng)前最小值小,就替換最小值索引,遍歷完成后,將第一個(gè)元素和最小值索引上的值交換。如上操作后,第一個(gè)元素就是數(shù)組中的最小值,下次遍歷就可以從索引 1 開始重復(fù)上述操作

以下是實(shí)現(xiàn)該算法的代碼

function selection(array) {checkArray(array);for (let i = 0; i < array.length - 1; i++) {let minIndex = i;for (let j = i + 1; j < array.length; j++) {minIndex = array[j] < array[minIndex] ? j : minIndex;}swap(array, i, minIndex);}return array; }

該算法的操作次數(shù)是一個(gè)等差數(shù)列?n + (n - 1) + (n - 2) + 1?,去掉常數(shù)項(xiàng)以后得出時(shí)間復(fù)雜度是?O(n * n)

歸并排序

歸并排序的原理如下。遞歸的將數(shù)組兩兩分開直到最多包含兩個(gè)元素,然后將數(shù)組排序合并,最終合并為排序好的數(shù)組。假設(shè)我有一組數(shù)組?[3, 1, 2, 8, 9, 7, 6],中間數(shù)索引是 3,先排序數(shù)組?[3, 1, 2, 8]?。在這個(gè)左邊數(shù)組上,繼續(xù)拆分直到變成數(shù)組包含兩個(gè)元素(如果數(shù)組長(zhǎng)度是奇數(shù)的話,會(huì)有一個(gè)拆分?jǐn)?shù)組只包含一個(gè)元素)。然后排序數(shù)組?[3, 1]?和?[2, 8]?,然后再排序數(shù)組?[1, 3, 2, 8]?,這樣左邊數(shù)組就排序完成,然后按照以上思路排序右邊數(shù)組,最后將數(shù)組?[1, 2, 3, 8]?和?[6, 7, 9]?排序

以下是實(shí)現(xiàn)該算法的代碼

function sort(array) {checkArray(array);mergeSort(array, 0, array.length - 1);return array; }function mergeSort(array, left, right) {// 左右索引相同說明已經(jīng)只有一個(gè)數(shù)if (left === right) return;// 等同于 `left + (right - left) / 2`// 相比 `(left + right) / 2` 來說更加安全,不會(huì)溢出// 使用位運(yùn)算是因?yàn)槲贿\(yùn)算比四則運(yùn)算快let mid = parseInt(left + ((right - left) >> 1));mergeSort(array, left, mid);mergeSort(array, mid + 1, right);let help = [];let i = 0;let p1 = left;let p2 = mid + 1;while (p1 <= mid && p2 <= right) {help[i++] = array[p1] < array[p2] ? array[p1++] : array[p2++];}while (p1 <= mid) {help[i++] = array[p1++];}while (p2 <= right) {help[i++] = array[p2++];}for (let i = 0; i < help.length; i++) {array[left + i] = help[i];}return array; }

以上算法使用了遞歸的思想。遞歸的本質(zhì)就是壓棧,每遞歸執(zhí)行一次函數(shù),就將該函數(shù)的信息(比如參數(shù),內(nèi)部的變量,執(zhí)行到的行數(shù))壓棧,直到遇到終止條件,然后出棧并繼續(xù)執(zhí)行函數(shù)。對(duì)于以上遞歸函數(shù)的調(diào)用軌跡如下

mergeSort(data, 0, 6) // mid = 3mergeSort(data, 0, 3) // mid = 1mergeSort(data, 0, 1) // mid = 0mergeSort(data, 0, 0) // 遇到終止,回退到上一步mergeSort(data, 1, 1) // 遇到終止,回退到上一步// 排序 p1 = 0, p2 = mid + 1 = 1// 回退到 `mergeSort(data, 0, 3)` 執(zhí)行下一個(gè)遞歸mergeSort(2, 3) // mid = 2mergeSort(3, 3) // 遇到終止,回退到上一步// 排序 p1 = 2, p2 = mid + 1 = 3// 回退到 `mergeSort(data, 0, 3)` 執(zhí)行合并邏輯// 排序 p1 = 0, p2 = mid + 1 = 2// 執(zhí)行完畢回退// 左邊數(shù)組排序完畢,右邊也是如上軌跡

該算法的操作次數(shù)是可以這樣計(jì)算:遞歸了兩次,每次數(shù)據(jù)量是數(shù)組的一半,并且最后把整個(gè)數(shù)組迭代了一次,所以得出表達(dá)式?2T(N / 2) + T(N)?(T?代表時(shí)間,N?代表數(shù)據(jù)量)。根據(jù)該表達(dá)式可以套用 該公式 得出時(shí)間復(fù)雜度為?O(N * logN)

快排

快排的原理如下。隨機(jī)選取一個(gè)數(shù)組中的值作為基準(zhǔn)值,從左至右取值與基準(zhǔn)值對(duì)比大小。比基準(zhǔn)值小的放數(shù)組左邊,大的放右邊,對(duì)比完成后將基準(zhǔn)值和第一個(gè)比基準(zhǔn)值大的值交換位置。然后將數(shù)組以基準(zhǔn)值的位置分為兩部分,繼續(xù)遞歸以上操作。

以下是實(shí)現(xiàn)該算法的代碼

function sort(array) {checkArray(array);quickSort(array, 0, array.length - 1);return array; }function quickSort(array, left, right) {if (left < right) {swap(array, , right)// 隨機(jī)取值,然后和末尾交換,這樣做比固定取一個(gè)位置的復(fù)雜度略低let indexs = part(array, parseInt(Math.random() * (right - left + 1)) + left, right);quickSort(array, left, indexs[0]);quickSort(array, indexs[1] + 1, right);} } function part(array, left, right) {let less = left - 1;let more = right;while (left < more) {if (array[left] < array[right]) {// 當(dāng)前值比基準(zhǔn)值小,`less` 和 `left` 都加一++less;++left;} else if (array[left] > array[right]) {// 當(dāng)前值比基準(zhǔn)值大,將當(dāng)前值和右邊的值交換// 并且不改變 `left`,因?yàn)楫?dāng)前換過來的值還沒有判斷過大小swap(array, --more, left);} else {// 和基準(zhǔn)值相同,只移動(dòng)下標(biāo)left++;}}// 將基準(zhǔn)值和比基準(zhǔn)值大的第一個(gè)值交換位置// 這樣數(shù)組就變成 `[比基準(zhǔn)值小, 基準(zhǔn)值, 比基準(zhǔn)值大]`swap(array, right, more);return [less, more]; }

該算法的復(fù)雜度和歸并排序是相同的,但是額外空間復(fù)雜度比歸并排序少,只需?O(logN),并且相比歸并排序來說,所需的常數(shù)時(shí)間也更少

面試題

Sort Colors:該題目來自 LeetCode,題目需要我們將?[2,0,2,1,1,0]?排序成?[0,0,1,1,2,2],這個(gè)問題就可以使用三路快排的思想

var sortColors = function(nums) {let left = -1;let right = nums.length;let i = 0;// 下標(biāo)如果遇到 right,說明已經(jīng)排序完成while (i < right) {if (nums[i] == 0) {swap(nums, i++, ++left);} else if (nums[i] == 1) {i++;} else {swap(nums, i, --right);}} };

#3.4 鏈表

反轉(zhuǎn)單向鏈表

該題目來自 LeetCode,題目需要將一個(gè)單向鏈表反轉(zhuǎn)。思路很簡(jiǎn)單,使用三個(gè)變量分別表示當(dāng)前節(jié)點(diǎn)和當(dāng)前節(jié)點(diǎn)的前后節(jié)點(diǎn),雖然這題很簡(jiǎn)單,但是卻是一道面試常考題

var reverseList = function(head) {// 判斷下變量邊界問題if (!head || !head.next) return head// 初始設(shè)置為空,因?yàn)榈谝粋€(gè)節(jié)點(diǎn)反轉(zhuǎn)后就是尾部,尾部節(jié)點(diǎn)指向 nulllet pre = nulllet current = headlet next// 判斷當(dāng)前節(jié)點(diǎn)是否為空// 不為空就先獲取當(dāng)前節(jié)點(diǎn)的下一節(jié)點(diǎn)// 然后把當(dāng)前節(jié)點(diǎn)的 next 設(shè)為上一個(gè)節(jié)點(diǎn)// 然后把 current 設(shè)為下一個(gè)節(jié)點(diǎn),pre 設(shè)為當(dāng)前節(jié)點(diǎn)while(current) {next = current.nextcurrent.next = prepre = currentcurrent = next}return pre };

#3.5 樹

二叉樹的先序,中序,后序遍歷

  • 先序遍歷表示先訪問根節(jié)點(diǎn),然后訪問左節(jié)點(diǎn),最后訪問右節(jié)點(diǎn)。
  • 中序遍歷表示先訪問左節(jié)點(diǎn),然后訪問根節(jié)點(diǎn),最后訪問右節(jié)點(diǎn)。
  • 后序遍歷表示先訪問左節(jié)點(diǎn),然后訪問右節(jié)點(diǎn),最后訪問根節(jié)點(diǎn)

遞歸實(shí)現(xiàn)

遞歸實(shí)現(xiàn)相當(dāng)簡(jiǎn)單,代碼如下

function TreeNode(val) {this.val = val;this.left = this.right = null; } var traversal = function(root) {if (root) {// 先序console.log(root);traversal(root.left);// 中序// console.log(root);traversal(root.right);// 后序// console.log(root);} };

對(duì)于遞歸的實(shí)現(xiàn)來說,只需要理解每個(gè)節(jié)點(diǎn)都會(huì)被訪問三次就明白為什么這樣實(shí)現(xiàn)了

非遞歸實(shí)現(xiàn)

非遞歸實(shí)現(xiàn)使用了棧的結(jié)構(gòu),通過棧的先進(jìn)后出模擬遞歸實(shí)現(xiàn)。

以下是先序遍歷代碼實(shí)現(xiàn)

function pre(root) {if (root) {let stack = [];// 先將根節(jié)點(diǎn) pushstack.push(root);// 判斷棧中是否為空while (stack.length > 0) {// 彈出棧頂元素root = stack.pop();console.log(root);// 因?yàn)橄刃虮闅v是先左后右,棧是先進(jìn)后出結(jié)構(gòu)// 所以先 push 右邊再 push 左邊if (root.right) {stack.push(root.right);}if (root.left) {stack.push(root.left);}}} }

以下是中序遍歷代碼實(shí)現(xiàn)

function mid(root) {if (root) {let stack = [];// 中序遍歷是先左再根最后右// 所以首先應(yīng)該先把最左邊節(jié)點(diǎn)遍歷到底依次 push 進(jìn)棧// 當(dāng)左邊沒有節(jié)點(diǎn)時(shí),就打印棧頂元素,然后尋找右節(jié)點(diǎn)// 對(duì)于最左邊的葉節(jié)點(diǎn)來說,可以把它看成是兩個(gè) null 節(jié)點(diǎn)的父節(jié)點(diǎn)// 左邊打印不出東西就把父節(jié)點(diǎn)拿出來打印,然后再看右節(jié)點(diǎn)while (stack.length > 0 || root) {if (root) {stack.push(root);root = root.left;} else {root = stack.pop();console.log(root);root = root.right;}}} }

以下是后序遍歷代碼實(shí)現(xiàn),該代碼使用了兩個(gè)棧來實(shí)現(xiàn)遍歷,相比一個(gè)棧的遍歷來說要容易理解很多

function pos(root) {if (root) {let stack1 = [];let stack2 = [];// 后序遍歷是先左再右最后根// 所以對(duì)于一個(gè)棧來說,應(yīng)該先 push 根節(jié)點(diǎn)// 然后 push 右節(jié)點(diǎn),最后 push 左節(jié)點(diǎn)stack1.push(root);while (stack1.length > 0) {root = stack1.pop();stack2.push(root);if (root.left) {stack1.push(root.left);}if (root.right) {stack1.push(root.right);}}while (stack2.length > 0) {console.log(s2.pop());}} }

中序遍歷的前驅(qū)后繼節(jié)點(diǎn)

實(shí)現(xiàn)這個(gè)算法的前提是節(jié)點(diǎn)有一個(gè)?parent?的指針指向父節(jié)點(diǎn),根節(jié)點(diǎn)指向?null

如圖所示,該樹的中序遍歷結(jié)果是?4, 2, 5, 1, 6, 3, 7

前驅(qū)節(jié)點(diǎn)

對(duì)于節(jié)點(diǎn) 2 來說,他的前驅(qū)節(jié)點(diǎn)就是 4 ,按照中序遍歷原則,可以得出以下結(jié)論

  • 如果選取的節(jié)點(diǎn)的左節(jié)點(diǎn)不為空,就找該左節(jié)點(diǎn)最右的節(jié)點(diǎn)。對(duì)于節(jié)點(diǎn) 1 來說,他有左節(jié)點(diǎn) 2 ,那么節(jié)點(diǎn) 2 的最右節(jié)點(diǎn)就是 5
  • 如果左節(jié)點(diǎn)為空,且目標(biāo)節(jié)點(diǎn)是父節(jié)點(diǎn)的右節(jié)點(diǎn),那么前驅(qū)節(jié)點(diǎn)為父節(jié)點(diǎn)。對(duì)于節(jié)點(diǎn) 5 來說,沒有左節(jié)點(diǎn),且是節(jié)點(diǎn) 2 的右節(jié)點(diǎn),所以節(jié)點(diǎn) 2 是前驅(qū)節(jié)點(diǎn)
  • 如果左節(jié)點(diǎn)為空,且目標(biāo)節(jié)點(diǎn)是父節(jié)點(diǎn)的左節(jié)點(diǎn),向上尋找到第一個(gè)是父節(jié)點(diǎn)的右節(jié)點(diǎn)的節(jié)點(diǎn)。對(duì)于節(jié)點(diǎn) 6 來說,沒有左節(jié)點(diǎn),且是節(jié)點(diǎn) 3 的左節(jié)點(diǎn),所以向上尋找到節(jié)點(diǎn) 1 ,發(fā)現(xiàn)節(jié)點(diǎn) 3 是節(jié)點(diǎn) 1 的右節(jié)點(diǎn),所以節(jié)點(diǎn) 1 是節(jié)點(diǎn) 6 的前驅(qū)節(jié)點(diǎn)

以下是算法實(shí)現(xiàn)

function predecessor(node) {if (!node) return// 結(jié)論 1if (node.left) {return getRight(node.left)} else {let parent = node.parent// 結(jié)論 2 3 的判斷while(parent && parent.right === node) {node = parentparent = node.parent}return parent} } function getRight(node) {if (!node) returnnode = node.rightwhile(node) node = node.rightreturn node }

后繼節(jié)點(diǎn)

對(duì)于節(jié)點(diǎn) 2 來說,他的后繼節(jié)點(diǎn)就是 5 ,按照中序遍歷原則,可以得出以下結(jié)論

  • 如果有右節(jié)點(diǎn),就找到該右節(jié)點(diǎn)的最左節(jié)點(diǎn)。對(duì)于節(jié)點(diǎn) 1 來說,他有右節(jié)點(diǎn) 3 ,那么節(jié)點(diǎn) 3 的最左節(jié)點(diǎn)就是 6
  • 如果沒有右節(jié)點(diǎn),就向上遍歷直到找到一個(gè)節(jié)點(diǎn)是父節(jié)點(diǎn)的左節(jié)點(diǎn)。對(duì)于節(jié)點(diǎn) 5 來說,沒有右節(jié)點(diǎn),就向上尋找到節(jié)點(diǎn) 2 ,該節(jié)點(diǎn)是父節(jié)點(diǎn) 1 的左節(jié)點(diǎn),所以節(jié)點(diǎn) 1 是后繼節(jié)點(diǎn) 以下是算法實(shí)現(xiàn)
function successor(node) {if (!node) return// 結(jié)論 1if (node.right) {return getLeft(node.right)} else {// 結(jié)論 2let parent = node.parent// 判斷 parent 為空while(parent && parent.left === node) {node = parentparent = node.parent}return parent} } function getLeft(node) {if (!node) returnnode = node.leftwhile(node) node = node.leftreturn node }

樹的深度

樹的最大深度:該題目來自 Leetcode,題目需要求出一顆二叉樹的最大深度

以下是算法實(shí)現(xiàn)

var maxDepth = function(root) {if (!root) return 0return Math.max(maxDepth(root.left), maxDepth(root.right)) + 1 };

對(duì)于該遞歸函數(shù)可以這樣理解:一旦沒有找到節(jié)點(diǎn)就會(huì)返回 0,每彈出一次遞歸函數(shù)就會(huì)加一,樹有三層就會(huì)得到3

總結(jié)

以上是生活随笔為你收集整理的前端-计算机基础的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。