字節跳動面經匯總 -- C++后端


本篇博文主要介紹2021秋招時匯總的一些字節跳動后端面試過程中可能遇到的一些問題。

malloc和new的區別

new/delete 是 C++關鍵字,需要編譯器支持。malloc/free 是庫函數,需要頭文件支持
使用 new 操作符申請內存分配時無須指定內存塊的大小,編譯器會根據類型信息自行計算。而 malloc 則需要顯式地指出所需內存的尺寸
new 操作符內存分配成功時,返回的是對象類型的指針,類型嚴格與對象匹配,無須進行類型轉換,故 new 是符合類型安全性的操作符。而 malloc 內存分配成功則是返回 void * ,需要通過強制類型轉換將 void*指針轉換成我們需要的類型
new 內存分配失敗時,會拋出 bad_alloc 異常。malloc 分配內存失敗時返回 NULL
new 會先調用 operator new 函數,申請足夠的內存(通常底層使用 malloc實現)。然后調用類型的構造函數,初始化成員變量,最后返回自定義類型指針。delete 先調用析構函數,然后調用 operator delete 函數釋放內存(通常底層使用 free 實現)。malloc/free 是庫函數,只能動態的申請和釋放內存,無法強制要求其做自定義類型對象構造和析構工作

排序算法時間復雜度

如何用Linux shell命令統計一個文本中各個單詞的個數

more log.txt | tr ' ' '\n' | sort | uniq -c

Linux下需要打開或者查看大文件

查看文件的幾行到幾行

sed -n '10,10000p' log
# 查看第10到10000行的數據

linux 怎么查看進程,怎么結束進程?原理是什么?

top, kill
ps -e 查看進程詳細信息

Linux怎么查看當前的負載情況

uptime命令主要用於獲取主機運行時間和查詢linux系統負載等信息
cat /proc/loadavg
tload
top
詳細

Http Code

MySQL的底層索引結構,InnoDB里面的B+Tree

詳細

不同引擎索引的區別

索引特征

i++是否原子操作

鎖的底層實現

B Tree 和 B+ Tree的區別

b樹和b+樹
詳細

B樹(B-tree)是一種樹狀數據結構,它能夠存儲數據、對其進行排序並允許以O(log n)的時間復雜度運行進行查找、順序讀取、插入和刪除的數據結構。B樹,概括來說是一個節點可以擁有多於2個子節點的二叉查找樹。與自平衡二叉查找樹不同,B-樹為系統最優化大塊數據的讀和寫操作。B-tree算法減少定位記錄時所經歷的中間過程,從而加快存取速度。普遍運用在數據庫和文件系統。
B 樹可以看作是對2-3查找樹的一種擴展,即他允許每個節點有M-1個子節點。M階B樹具有以下特征:

  1. 根節點至少有兩個子節點
  2. 每個節點有M-1個key,並且以升序排列
  3. 位於M-1和M key的子節點的值位於M-1 和M key對應的Value之間
  4. 其它節點至少有M/2個子節點

B+樹
B+樹是對B樹的一種變形樹,它與B樹的差異在於:

  • 有k個子結點的結點必然有k個關鍵碼
  • 非葉結點僅具有索引作用,跟記錄有關的信息均存放在葉結點中
  • 樹的所有葉結點構成一個有序鏈表,可以按照關鍵碼排序的次序遍歷全部記錄

B樹與B+樹的區別:

  • B樹每個節點都存儲數據,所有節點組成這棵樹。B+樹只有葉子節點存儲數據(B+數中有兩個頭指針:一個指向根節點,另一個指向關鍵字最小的葉節點),葉子節點包含了這棵樹的所有數據,所有的葉子結點使用鏈表相連,便於區間查找和遍歷,所有非葉節點起到索引作用。
  • B樹中葉節點包含的關鍵字和其他節點包含的關鍵字是不重復的,B+樹的索引項只包含對應子樹的最大關鍵字和指向該子樹的指針,不含有該關鍵字對應記錄的存儲地址。
  • B樹中每個節點(非根節點)關鍵字個數的范圍為m/2(向上取整)-1,m-1,並且具有n個關鍵字的節點包含(n+1)棵子樹。B+樹中每個節點(非根節點)關鍵字個數的范圍為m/2(向上取整),m,具有n個關鍵字的節點包含(n)棵子樹。
  • B+樹中查找,無論查找是否成功,每次都是一條從根節點到葉節點的路徑。

B樹的優點:

  • B樹的每一個節點都包含key和value,因此經常訪問的元素可能離根節點更近,因此訪問也更迅速。

B+樹的優點:

  • 所有的葉子結點使用鏈表相連,便於區間查找和遍歷。B樹則需要進行每一層的遞歸遍歷。相鄰的元素可能在內存中不相鄰,所以緩存命中性沒有B+樹好。
  • b+樹的中間節點不保存數據,能容納更多節點元素。

B樹與B+樹的共同優點:

  • 考慮磁盤IO的影響,它相對於內存來說是很慢的。數據庫索引是存儲在磁盤上的,當數據量大時,就不能把整個索引全部加載到內存了,只能逐一加載每一個磁盤頁(對應索引樹的節點)。所以我們要減少IO次數,對於樹來說,IO次數就是樹的高度,而“矮胖”就是b樹的特征之一,m的大小取決於磁盤頁的大小。

MySQL索引的發展過程?是一來就是B+Tree的么?從 沒有索引、hash、二叉排序樹、AVL樹、B樹、B+樹 聊

詳細1
詳細2

MySQL里面的事務,說說什么是事務

數據庫事務(Database Transaction) ,是指作為單個邏輯工作單元執行的一系列操作,要么完全地執行,要么完全地不執行。 事務處理可以確保除非事務性單元內的所有操作都成功完成,否則不會永久更新面向數據的資源。通過將一組相關操作組合為一個要么全部成功要么全部失敗的單元,可以簡化錯誤恢復並使應用程序更加可靠。一個邏輯工作單元要成為事務,必須滿足所謂的 ACID(原子性、一致性、隔離性和持久性)屬性。事務是數據庫運行中的邏輯工作單位,由 DBMS 中的事務管理子系統負責事務的處理。

MySQL里面有那些事務級別,並且不同的事務級別會出現什么問題

讀未提交 (臟讀):最低的隔離級別,什么都不需要做,一個事務可以讀到另一個事務未提交的結果。所有的並發事務問題都會發生
讀提交 (讀舊數據,不可重復讀問題):只有在事務提交后,其更新結果才會被其他事務看見。可以解決臟讀問題。
可重復讀 (解決了臟讀但是有幻影讀):在一個事務中,對於同一份數據的讀取結果總是相同的,無論是否有其他事務對這份數據進行操作,以及這個事務是否提交。可以解決臟讀、不可重復讀。
串行化:事務串行化執行,隔離級別最高,犧牲了系統的並發性。可以解決並發事務的所有問題。

不可重復讀和幻讀的區別

  1. 不可重復讀是讀異常,但幻讀則是寫異常
  2. 不可重復讀是讀異常的意思是,如果你不多select幾次,你是發現不了你曾經select過的數據行已經被其他人update過了。避免不可重復讀主要靠一致性快照
  3. 幻讀是寫異常的意思是,如果不自己insert一下,你是發現不了其他人已經偷偷insert過相同的數據了。解決幻讀主要靠間隙鎖

數據庫持久性是怎么實現的?

詳細
持久性是指一個事務一旦被提交了,那么對數據庫中的數據的改變就是永久性的,即便是在數據庫系統遇到故障的情況下也不會丟失提交事務的操作。
MySQL采用了一種叫WAL(Write Ahead Logging)提前寫日志的技術。意思就是說,發生了數據修改操作先寫日志記錄下來,等不忙的時候再持久化到磁盤。這里提到的日志就是redo log。
redo log稱為重做日志,當有一條記錄需要修改的時候,InnoDB引擎會先把這條記錄寫到redo log里面。redo log是物理格式日志,它記錄的是對於每個頁的修改。
redo log是由兩部分組成的:一是內存中的重做日志緩沖(redo log buffer);二是用來持久化的重做日志文件(redo log file)。為了消耗不必要的IO操作,事務再執行過程中產生的redo log首先會redo log buffer中,之后再統一存入redo log file刷盤進行持久化,這個動作稱為fsync
binlog記錄了mysql執行更改了所有操作,但不包含select和show這類本對數據本身沒有更改的操作。但是不是說對數據本身沒有修改就不會記錄binlog日志。

  • binlog是mysql自帶的,他會記錄所有存儲引擎的日志文件。而redo log是InnoDB特有的,他只記錄該存儲引擎產生的日志文件
  • binlog是邏輯日志,記錄這個語句具體操作了什么內容。Redo log是物理日志,記錄的是每個頁的更改情況
  • redo log是循環寫,只有那么大的空間。binlog采用追加寫入,當一個binlog文件寫到一定大小后會切換到下一個文件

更新一條語句的流程

  1. 首先執行器調用引擎獲取數據,如果數據在內存中就直接返回;否則先從磁盤中讀取數據,寫入內存后再返回。
  2. 修改數據后再調用引擎接口寫入這行數據
  3. 引擎層將這行數據更新到內存中,然后將更新操作寫入redo log,這時候redo log標記為prepare狀態。然后告訴執行器我處理完了,可以提交事務了。
  4. 執行器生成這個操作的binlog,並把binlog寫入磁盤,然后調用引擎提交事務
  5. 引擎收到commit命令后,把剛才寫入的redo log改成commit狀態
    這里使用了兩階段提交prepare階段和commit階段

數據庫回表是什么?

詳細
Innodb的索引存在兩類,一類是聚簇索引一類是非聚簇索引,Innodb有且僅有一個聚簇索引。1. 如果表定義了PK,則PK就是聚集索引;2. 如果表沒有定義PK,則第一個not NULL unique列是聚集索引;否則,InnoDB會創建一個隱藏的row-id作為聚集索引;
InnoDB 聚集索引 的葉子節點存儲行記錄,而普通索引的葉子節點存儲主鍵值
在使用聚簇索引時,可以一步直接獲取到記錄值,而使用普通索引時,會首先獲取記錄的PK,然后再從聚簇索引中查找對應的記錄,這個過程叫做回表

索引覆蓋

理解方式一:就是select的數據列只用從索引中就能夠取得,不必讀取數據行,換句話說查詢列要被所建的索引覆蓋
理解方式二:索引是高效找到行的一個方法,但是一般數據庫也能使用索引找到一個列的數據,因此它不必讀取整個行。畢竟索引葉子節點存儲了它們索引的數據;當能通過讀取索引就可以得到想要的數據,那就不需要讀取行了。一個索引包含了(或覆蓋了)滿足查詢結果的數據就叫做覆蓋索引
理解方式三:是非聚集復合索引的一種形式,它包括在查詢里的Select、Join和Where子句用到的所有列(即建索引的字段正好是覆蓋查詢條件中所涉及的字段,也即,索引包含了查詢正在查找的數據)。
總結:要查找的數據可以都在索引中出現,而不需要再去查表獲取完整記錄
為了實現索引覆蓋可以將被查詢的字段,建立到聯合索引里去

數據庫讀寫鎖發生死鎖的情景

詳細

  • MyISAM中不會出現死鎖
    在MyISAM中只用到表鎖,不會有死鎖的問題,鎖的開銷也很小,但是相應的並發能力很差。
    解析:MyISAM不支持事務,即每次的讀寫都會隱性的加上讀寫鎖,而我們知道讀鎖是共享的,寫鎖是獨占的,意味着當一個Session在寫時,另一個Session必須等待。
  • InnoDB中會出現死鎖
    InnoDB中實用了行鎖和表鎖,當未命中索引時,會自動退化為表鎖。
    解決方法為InnoDB中的MVCC機制

為什么推薦主鍵使用自增的整型,MySQL為什么主鍵自增好

為什么推薦主鍵:Innodb底層是B+樹,數據和索引放在一起,因此需要一個主鍵作為索引,從而存儲數據
為什么要自增:當新存儲一條數據時,只需要向B+樹后面的葉子節點插入即可,而不需要B+ 樹為保持有序而進行旋轉
為什么要整型:整形作為索引,容易直接判斷大小而保持有序,使用String,相對於整數而言,不易判斷大小

vector的底層實現,擴容機制

詳細
使用一段連續的內存來存儲數據,同時在數據結構中保存了三個指針來標記內存地址,首先是指向vector中數據的起始位置的_Myfirst指針,最后一個數據的位置的_Mylst指針,以及一個指向連續內存空間末尾的_Myend指針
當 vector 的大小和容量相等(size==capacity)也就是滿載時,如果再向其添加元素,那么 vector 就需要擴容。vector 容器擴容的過程需要經歷以下 3 步:

  1. 完全棄用現有的內存空間,重新申請更大的內存空間;
  2. 將舊內存空間中的數據,按原有順序移動到新的內存空間中;
  3. 最后將舊的內存空間釋放
    不同的編譯器,vector 有不同的擴容大小。在 vs 下是 1.5 倍,在 GCC 下是 2 倍
    采用成倍方式擴容,可以保證常數的時間復雜度,而增加指定大小的容量只能達到O(n)的時間復雜度,因此,使用成倍的方式擴容
    過大的倍數將導致大量空間的浪費
    為什么擴容二倍

MySQL中如果使用like進行模糊匹配的時候,是否會使用索引

mysql在使用like查詢的時候只有使用后面的%時,才會使用到索引

Volatile的作用,Volatile如何保證可見性的?以及如何實現可見性的機制

volatile 關鍵字是一種類型修飾符,用它聲明的類型變量表示可以被某些編譯器未知的因素更改,比如:操作系統、硬件或者其它線程等。遇到這個關鍵字聲明的變量,編譯器對訪問該變量的代碼就不再進行優化,從而可以提供對特殊地址的穩定訪問。聲明時語法:int volatile vInt; 當要求使用 volatile 聲明的變量的值的時候,系統總是重新從它所在的內存讀取數據,即使它前面的指令剛剛從該處讀取過數據。而且讀取的數據立刻被保存
volatile 用在如下的幾個地方:

  1. 中斷服務程序中修改的供其它程序檢測的變量需要加 volatile;
  2. 多任務環境下各任務間共享的標志應該加 volatile;
  3. 存儲器映射的硬件寄存器通常也要加 volatile 說明,因為每次對它的讀寫都可能有不同意義
    詳細.
    volatile作用:
    鎖總線,其它CPU對內存的讀寫請求都會被阻塞,直到鎖釋放,不過實際后來的處理器都采用鎖緩存替代鎖總線,因為鎖總線的開銷比較大,鎖總線期間其他CPU沒法訪問內存
    lock后的寫操作會回寫已修改的數據,同時讓其它CPU相關緩存行失效,從而重新從主存中加載最新的數據
    不是內存屏障卻能完成類似內存屏障的功能,阻止屏障兩遍的指令重排序

如果大量的使用Volatile存在什么問題

操作系統的線程,以及它的狀態

線程的基本狀態:
1.新建
new語句創建的線程對象處於新建狀態,此時它和其他java對象一樣,僅被分配了內存。
2.等待
當線程在new之后,並且在調用start方法前,線程處於等待狀態。
3.就緒
當一個線程對象創建后,其他線程調用它的start()方法,該線程就進入就緒狀態。處於這個狀態的線程位於Java虛擬機的可運行池中,等待cpu的使用權。
4.運行狀態
處於這個狀態的線程占用CPU,執行程序代碼。在並發運行環境中,如果計算機只有一個CPU,那么任何時刻只會有一個線程處於這個狀態。只有處於就緒狀態的線程才有機會轉到運行狀態。
5. 阻塞狀態
阻塞狀態是指線程因為某些原因放棄CPU,暫時停止運行。當線程處於阻塞狀態時,Java虛擬機不會給線程分配CPU,直到線程重新進入就緒狀態,它才會有機會獲得運行狀態。
6.死亡狀態
當線程執行完run()方法中的代碼,或者遇到了未捕獲的異常,就會退出run()方法,此時就進入死亡狀態,該線程結束生命周期。

進程和線程(進程與線程)的區別以及使用場景

線程產生的原因:進程可以使多個程序能並發執行,以提高資源的利用率和系統的吞吐量;但是其具有一些缺點:

  • 進程在同一時間只能干一件事
  • 進程在執行的過程中如果阻塞,整個進程就會掛起,即使進程中有些工作不依賴於等待的資源,仍然不會執行。
  • 因此,操作系統引入了比進程粒度更小的線程,作為並發執行的基本單位,從而減少程序在並發執行時所付出的時空開銷,提高並發性

進程是資源分配的最小單位,線程是操作系統進行執行和調度的最小單位

  1. 同一進程的線程共享本進程的地址空間,而進程之間則是獨立的地址空間;
  2. 同一進程內的線程共享本進程的資源,但是進程之間的資源是獨立的;
  3. 一個進程崩潰后,在保護模式下不會對其他進程產生影響,但是一個線程崩潰整個進程崩潰,所以多進程比多線程健壯;
  4. 進程切換,消耗的資源大。所以涉及到頻繁的切換,使用線程要好於進程;
  5. 兩者均可並發執行;
  6. 每個獨立的進程有一個程序的入口、程序出口。但是線程不能獨立執行,必須依存在應用程序中,由應用程序提供多個線程執行控制

使用場景

  • 需要頻繁創建銷毀的優先用線程
    最常見的應用就是 Web 服務器了,來一個連接建立一個線程,斷了就銷毀線程
  • 需要進行大量計算的優先使用線程
    所謂大量計算,當然就是要耗費很多 CPU,切換頻繁了,這種情況下線程是最合適的
    最常見的是圖像處理、算法處理
  • 強相關的處理用線程,弱相關的處理用進程
  • 可能要擴展到多機分布的用進程,多核分布的用線程

線程私有:線程棧,寄存器,程序寄存器
共享:堆,地址空間,全局變量,靜態變量
進程私有:地址空間,堆,全局變量,棧,寄存器
共享:代碼段,公共數據,進程目錄,進程 ID

為什么線程創建和撤銷開銷大

當從一個線程切換到另一個線程時,不僅會發生線程上下文切換,還會發生特權模式切換。
既然是線程切換,那么一定涉及線程狀態的保存和恢復,包括寄存器、棧等私有數據。另外,線程的調度是需要內核級別的權限的(操作CPU和內存),也就是說線程的調度工作是在內核態完成的,因此會有一個從用戶態到內核態的切換。而且,不管是線程本身的切換還是特權模式的切換,都要進行CPU的上下文切換

線程和協程的由來和作用

協程,又稱微線程。協程看上去也是子程序,但執行過程中,在子程序內部可中斷,然后轉而執行別的子程序,在適當的時候再返回來接着執行。
和多線程比,協程最大的優勢就是協程極高的執行效率。因為子程序切換不是線程切換,而是由程序自身控制,因此,沒有線程切換的開銷,和多線程比,線程數量越多,協程的性能優勢就越明顯
第二大優勢就是不需要多線程的鎖機制,因為只有一個線程,也不存在同時寫變量沖突,在協程中控制共享資源不加鎖,只需要判斷狀態就好了,所以執行效率比多線程高很多
在協程上利用多核 CPU —— 多進程+協程,既充分利用多核,又充分發揮協程的高效率,
可獲得極高的性能

多線程的通信和同步,多線程訪問同一個對象怎么辦

  • 互斥鎖
  • 條件變量
  • 讀寫鎖
  • 信號

查看端口號或者進程號,使用什么命令

查看程序對應的進程號: ps -ef | grep 進程名字
查看進程號所占用的端口號: netstat -nltp | grep 進程號
查看端口號所使用的進程號: lsof -i:端口號

信號量與mutex和自旋鎖的區別

信號量(semaphore)用在多線程多任務同步的,一個線程完成了某一個動作就通過信號量告訴別的線程,別的線程再進行某些動作。而互斥鎖(Mutual exclusion,縮寫 Mutex)是用在多線程多任務互斥的,一個線程占用了某一個資源,那么別的線程就無法訪問,直到這個線程unlock,其他的線程才開始可以利用這個資源
自旋鎖與前兩者的區別是自旋鎖不會引起調用者睡眠,如果自旋鎖已經被別的執行單元保持,調用者就一直循環在那里看是否該自旋鎖的保持者已經釋放了鎖

非對稱加密與對稱加密

對稱加密算法:加密效率高,速度快,適合大數據量加密。DES/AES
非對稱加密算法:算法復雜,加密速度慢,安全性更高。結合對稱加密使用。RSA、DH

在兩列屬性上分別建索引,則某個查詢語句使用 where att1 = * and att2 = * 會怎么使用索引

一次查詢只使用一個索引,因為每個索引都代表了一顆樹,使用多個索引所帶來的增益遠小於增加的性能消耗。
對於聯合索引來說會使用最左匹配 -- 在MySQL的user表中,對a,b,c三個字段建立聯合索引,根據查詢字段的位置不同來決定,如查詢a, a,b a,b,c a,c 都可以走索引,其他條件的查詢不能走索引
對於多個單列索引來說,MySQL會試圖選擇一個限制最嚴格的索引。但是,即使是限制最嚴格的單列索引,它的限制能力也肯定遠遠低於在多列上的多列索引

通過兩個索引查詢出來的結果,會進行什么要的操作?交集,並集

MySQL中遇到一些慢查詢,有什么解決方法

  • 定位慢查詢

根據慢查詢日志定位慢查詢sql

  1. slow_query_log 默認是off關閉的,使用時,需要改為on 打開
  2. slow_query_log_file 記錄的是慢日志的記錄文件
  3. long_query_time 默認是10S,每次執行的sql達到這個時長,就會被記錄
  • 優化方案

優化數據庫結構
分解關聯查詢: 很多高性能的應用都會對關聯查詢進行分解,就是可以對每一個表進行一次單表查詢,然后將查詢結果在應用程序中進行關聯,很多場景下這樣會更高效。
增加索引
建立視圖
優化查詢語句
添加存儲過程
冗余保存數據

有了解過IO多路復用技術是個什么樣的原理

I/O多路復用,I/O就是指的我們網絡I/O,多路指多個TCP連接(或多個Channel),復用指復用一個或少量線程。串起來理解就是很多個網絡I/O復用一個或少量的線程來處理這些連接。

通過一個線程,同時連接多個線程會不會存在多個線程切換

在操作系統中,有高速緩存,主存,虛擬內存,外存,有知道它們之間有什么樣的關系,以及它們的作用是啥

緩存: 在CPU同時處理很多數據,而又不可能同時進行所有數據的傳輸的情況,把優先級低的數據暫時放入緩存中,等優先級高的數據處理完畢后再把它們從緩存中拿出來進行處理
主存:主存就是內存,是直接與CPU交換信息的存儲器,指CPU能夠通過指令中的地址碼直接訪問的存儲器,常用於存放處於活動狀態的程序和數據
虛擬內存:當運行數據超過內存限度,部分數據自動“溢出”,這時系統會將硬盤上的部分空間模擬成內存——虛擬內存,並且將暫時不運行的程序或不使用的數據存放到虛擬內存中等待需要時調用
輔存就是外存: 硬盤與磁盤、光盤、軟盤、U盤等

缺頁的產生和換頁算法

缺頁中斷:進程線性地址空間里的頁面不必常駐內存,在執行一條指令時,如果發現他要訪問的頁沒有在內存中(存在位為0),那么停止該指令的執行,並產生一個頁不存在異常,對應的故障處理程序可通過從外存加載該頁到內存的方法來排除故障,之后,原先引起的異常的指令就可以繼續執行,而不再產生異常
頁面置換算法:將新頁面調入內存時,如果內存中所有的物理頁都已經分配出去,就要按某種策略來廢棄某個頁面,將其所占據的物理頁釋放出來,好的算法,讓缺頁率降低。

  1. 先進先出調度算法(FIFO)
  2. 最近最少調度算法(LFU,根據時間判斷):利用局部性原理,根據一個作業在執行過程中過去的頁面訪問歷史來推測未來的行為。它認為過去一段時間里不曾被訪問過的頁面,在最近的將來可能也不會再被訪問。所以,這種算法的實質是:當需要淘汰一個頁面時,總是選擇在最近一段時間內最久不用的頁面予以淘汰。
  3. 最近最不常用調度算法(LRU,根據使用頻率判斷
struct DLinkedNode {
    int key, value;
    DLinkedNode* prev;
    DLinkedNode* next;
    DLinkedNode(): key(0), value(0), prev(nullptr), next(nullptr) {}
    DLinkedNode(int _key, int _value): key(_key), value(_value), prev(nullptr), next(nullptr) {}
};

class LRUCache {
private:
    unordered_map<int, DLinkedNode*> cache;
    DLinkedNode* head;
    DLinkedNode* tail;
    int size;
    int capacity;

public:
    LRUCache(int _capacity): capacity(_capacity), size(0) {
        // 使用偽頭部和偽尾部節點
        head = new DLinkedNode();
        tail = new DLinkedNode();
        head->next = tail;
        tail->prev = head;
    }
    
    int get(int key) {
        if (!cache.count(key)) {
            return -1;
        }
        // 如果 key 存在,先通過哈希表定位,再移到頭部
        DLinkedNode* node = cache[key];
        moveToHead(node);
        return node->value;
    }
    
    void put(int key, int value) {
        if (!cache.count(key)) {
            // 如果 key 不存在,創建一個新的節點
            DLinkedNode* node = new DLinkedNode(key, value);
            // 添加進哈希表
            cache[key] = node;
            // 添加至雙向鏈表的頭部
            addToHead(node);
            ++size;
            if (size > capacity) {
                // 如果超出容量,刪除雙向鏈表的尾部節點
                DLinkedNode* removed = removeTail();
                // 刪除哈希表中對應的項
                cache.erase(removed->key);
                // 防止內存泄漏
                delete removed;
                --size;
            }
        }
        else {
            // 如果 key 存在,先通過哈希表定位,再修改 value,並移到頭部
            DLinkedNode* node = cache[key];
            node->value = value;
            moveToHead(node);
        }
    }

    void addToHead(DLinkedNode* node) {
        node->prev = head;
        node->next = head->next;
        head->next->prev = node;
        head->next = node;
    }
    
    void removeNode(DLinkedNode* node) {
        node->prev->next = node->next;
        node->next->prev = node->prev;
    }

    void moveToHead(DLinkedNode* node) {
        removeNode(node);
        addToHead(node);
    }

    DLinkedNode* removeTail() {
        DLinkedNode* node = tail->prev;
        removeNode(node);
        return node;
    }
};
  1. 最佳置換算法(OPT):從主存中移出永遠不再需要的頁面;如無這樣的頁面存在,則選擇最長時間不需要訪問的頁面。於所選擇的被淘汰頁面將是以后永不使用的,或者是在最長時間內不再被訪問的頁面,這樣可以保證獲得最低的缺頁率

面向對象采用的設計模式有哪些

  • 在軟件工程中,軟件設計模式是通用的,可重用的在給定上下文中解決軟件設計中常見問題的解決方案

單例模式(有的叫單元素模式,單態模式)
工廠模式
觀察者模式
命令鏈模式
策略模式

設計模式的六大原則

為什么要有補碼?
(為了更方便的實現減法運算)

一致性哈希

面向對象有哪些設計原則

OCP原則(也叫開閉原則): 開閉原則就是說對擴展開放,對修改關閉。
SRP原則(職責單一原則): 一個類只負責一項職責,可以降低類的復雜度,提高類的可讀性,提高系統的可維護性,當修改一個功能時,可以顯著降低對其他功能的影響。
OCP原則(里氏替換原則):任何基類可以出現的地方,子類一定可以出現。通俗的理解即為子類可以擴展父類的功能,但不能改變父類原有的功能。
DIP原則(依賴倒置原則):高層模塊不應該依賴低層模塊,二者都應該依賴其抽象;抽象不應該依賴細節;細節應該依賴抽象。通俗點說:要求對抽象進行編程,不要對實現進行編程,這樣就降低了客戶與實現模塊間的耦合。
LoD法則(迪米特法則):一個對象應該對其他對象保持最少的了解。通俗的來講,就是一個類對自己依賴的類知道的越少越好。也就是說,對於被依賴的類來說,無論邏輯多么復雜,都盡量地的將邏輯封裝在類的內部,對外除了提供的public方法,不對外泄漏任何信息。
接口隔離原則:建立單一接口,不要建立龐大臃腫的接口,盡量細化接口,接口中的方法盡量少。也就是說,我們要為各個類建立專用的接口,而不要試圖去建立一個很龐大的接口供所有依賴它的類去調用。

說說你對於 “不要用共享內存來通信,而應該用通信來共享內存” 的理解

在鎖模式中,一塊內存可以被多個線程同時看到,所以叫共享內存。線程之間通過改變內存中的數據來通知其他線程發生了什么,所以是通過共享內存來通信。鎖是為了保護一個線程對內存操作的邏輯完整性而引入的一種約定,注意是一種約定而不是規則(一個線程可以不獲取鎖就操作內存,也可以解鎖其他線程加的鎖從而破壞保護,這種錯誤很難發現)。這種約定要每個線程的編寫人員自覺遵守,否則就會出現多線程問題,如數據被破壞,死鎖,飢餓等。
在go模式中,一塊內存同一時間只能被一個線程看到,另外一個線程要操作這塊內存,需要當前線程讓渡所有權,這個所有權的讓渡過程是“通信”。通信的原子性由channel封裝好了,內存同一時間只能被同一線程使用,所以這種模式下不需要顯示的鎖。然而go模式也有約定,如果傳遞的是內存的指針,或者是控制消息,還是等於共享了內存,還是要保證將所有權讓渡后, 不能再操作這塊內存。

什么是雙向鏈表

雙向鏈表也叫雙鏈表,是鏈表的一種,它的每個數據結點中都有兩個指針,分別指向直接后繼和直接前驅。所以,從雙向鏈表中的任意一個結點開始,都可以很方便地訪問它的前驅結點和后繼結點。

操作系統內存管理(分頁、分段、段頁式)

頁式內存管理,內存分成固定長度的一個個頁片。操作系統為每一個進程維護了一個從虛擬地址到物理地址的映射關系的數據結構,叫頁表,頁表的內容就是該進程的虛擬地址到物理地址的一個映射。頁表中的每一項都記錄了這個頁的基地址。通過頁表,由邏輯地址的高位部分先找到邏輯地址對應的頁基地址,再由頁基地址偏移一定長度就得到最后的物理地址,偏移的長度由邏輯地址的低位部分決定。一般情況下,這個過程都可以由硬件完成,所以效率還是比較高的。
頁式內存管理的優點就是比較靈活,內存管理以較小的頁為單位,方便內存換入換出和擴充地址空間。
分段存儲管理方式的目的,主要是為了滿足用戶(程序員)在編程和使用上多方面的要求,其中有些要求是其他幾種存儲管理方式所難以滿足的。因此,這種存儲管理方式已成為當今所有存儲管理方式的基礎。

  • 分頁與分段的區別
    頁是信息的物理單位,分頁是為實現離散分配方式,以消減內存的外零頭,提高內存的利用率。或者說,分頁僅僅是由於系統管理的需要而不是用戶的需要。段則是信息的邏輯單位,它含有一組其意義相對完整的信息。分段的目的是為了能更好地滿足用戶的需要
    頁的大小固定且由系統決定,而段的長度卻不固定
    分頁的作業地址空間是一維的,即單一的線性地址空間;而分段的作業地址空間則是二維的
    分頁系統能有效地提高內存利用率,而分段系統則能很好地滿足用戶需求。

段頁式系統的基本原理,是分段和分頁原理的結合,即先將用戶程序分成若干個段,再把每個段分成若干個頁,並為每一個段賦予一個段名。在段頁式系統中,地址結構由段號、段內頁號和頁內地址三部分所組成。

瀏覽器輸入網址到渲染的過程

詳細

  • 瀏覽器構建HTTP Request請求
  • 網絡傳輸
  • 服務器構建HTTP Response 響應
  • 網絡傳輸
  • 瀏覽器渲染頁面

DNS解析URL地址、生成HTTP請求報文、構建TCP連接、使用IP協議選擇傳輸路線、數據鏈路層保證數據的可靠傳輸、物理層將數據轉換成電子、光學或微波信號進行傳輸

DNS解析過程

  • DNS 協議運行在 UDP 協議之上,使用端口號 53,用於將域名轉換為IP地址
  1. 瀏覽器先檢查自身緩存中有沒有被解析過這個域名對應的 ip 地址
  2. 如果瀏覽器緩存沒有命中,瀏覽器會檢查操作系統緩存中有沒有對應的已解析過的結果。在 windows 中可通過 c 盤里 hosts 文件來設置
  3. 還沒命中,請求本地域名服務器來解析這個域名,一般都會在本地域名服務器找到
  4. 本地域名服務器沒有命中,則去根域名服務器請求解析
  5. 根域名服務器返回給本地域名服務器一個所查詢域的主域名服務器
  6. 本地域名服務器向主域名服務器發送請求
  7. 接受請求的主域名服務器查找並返回這個域名對應的域名服務器的地址
  8. 域名服務器根據映射關系找到 ip 地址,返回給本地域名服務器
  9. 本地域名服務器緩存這個結果
  10. 本地域名服務器將該結果返回給用戶

https講一下?密鑰怎么交換的?私鑰存儲在哪里

HTTPs 是以安全為目標的 HTTP 通道,簡單講是 HTTP 的安全版,即HTTP 下加入 SSL 層,HTTPS 的安全基礎是 SSL,因此加密的詳細內容就需要 SSL
HTTPs的握手過程包含五步

  1. 瀏覽器請求連接
  2. 服務器返回證書:證書里面包含了網站地址,加密公鑰,以及證書的頒發機構等信息,服務器采用非對稱加密算法(RSA)生成兩個秘鑰,私鑰自己保留
  3. 瀏覽器收到證書后作以下工作:
    3.1 驗證證書的合法性
    3.2 生成隨機(對稱)密碼,取出證書中提供的公鑰對隨機密碼加密;瀏覽器即客戶端使用非對稱加密來加密對稱加密規則,對稱加密用於加密后續傳輸的信息
    3.3 將之前生成的加密隨機密碼等信息發送給網站
  4. 服務器收到消息后作以下的操作
    4.1 使用自己的私鑰解密瀏覽器用公鑰加密后的消息,並驗證 HASH 是否與瀏覽器發來的一致;獲得瀏覽器發過來的對稱秘鑰
    4.2 使用加密的隨機對稱密碼加密一段消息,發送給瀏覽器
  5. 瀏覽器解密並計算握手消息的 HASH:如果與服務端發來的 HASH 一致,此時握手過程結束,之后進行通信

http的流程

每個萬維網的網點都有一個服務器進程,它不斷的監聽TCP端口80,以便發現是否有瀏覽器向它發出連接請求,一旦監聽到連接建立請求,就通過三次握手建立TCP連接,然后瀏覽器會向服務器發出瀏覽某個頁面的請求,服務器接着返回所請求的頁面作為響應,然后TCP連接就被釋放了。
這些響應和請求報文都遵循一定的格式,這就是HTTP協議所規定的。

SSL加密

  1. 客戶端向服務器端索要並驗證公鑰
  2. 雙方協商生成”對話密鑰”。客戶端用公鑰對對話秘鑰進行加密
  3. 服務器通過私鑰解密出對話秘鑰
  4. 雙方采用”對話密鑰”進行加密通信
  • 對稱加密算法: 加密效率高,速度快,適合大數據量加密。DES/AES
  • 非對稱加密算法:算法復雜,加密速度慢,安全性更高。結合對稱加密使用。RSA、DH
  • 私鑰存儲在服務器上

session和cookie

  • cookie 是一種發送到客戶瀏覽器的文本串句柄,並保存在客戶機硬盤上,可以用來在某個WEB站點會話間持久的保持數據。
  • Session的本質上也是cookie,但是不同點是存在服務器上的。這就導致,你如果使用cookie,你關閉瀏覽器之后,就丟掉Cookie了,但是如果關掉瀏覽器,重新打開之后,發現還有相應的信息,那就說明用的是Session。因為cookie是存在本地的,所以也會有相應的安全問題,攻擊者可以去偽造他,填寫相應的字段名,就能登錄你的賬戶,還有如果cookie的有效期很長的話,也不安全。
  • session 由服務器產生,對於客戶端,只存儲session id在cookie中

http和https的區別

  • https 協議需要到 ca 申請證書,一般免費證書較少,因而需要一定費用
  • http 是超文本傳輸協議,信息是明文傳輸,https 則是具有安全性的 ssl 加密傳輸協議
  • http 和 https 使用的是完全不同的連接方式,用的端口也不一樣,前者是 80,后者是 443
  • http 的連接很簡單,是無狀態的;HTTPS 協議是由 SSL+HTTP 協議構建的可進行加密傳輸、身份認證的網絡協議,比 http 協議安全

https的安全外殼是怎么實現的

HTTPS就是在原HTTP的基礎上加上一層用於數據加密、解密、校驗、身份認證的安全層SSL/TSL,用於解決HTTP存在的安全隱患
信息加密:所有信息都是加密傳播,第三方無法竊聽;內容經過對稱加密,每個連接生成一個唯一的加密密鑰;
身份認證:配備了身份認證,第三方無法偽造服務端(客戶端)的身份
數據完整性校驗:內容傳輸經過完整性校驗,一旦報文被篡改,通信雙方會立刻發現

TCP TIMEWAIT講一下?為啥需要這個?

當斷開連接時,客戶端發送完ACK將處於TIME WAIT狀態,保持2MSL,之后完全斷開意義在於:

  1. 保證最后一次握手報文能到服務端,能進行超時重傳
  2. 2MSL 后,這次連接的所有報文都會消失,不會影響下一次連接

說一下TCP/IP

TCP/IP協議是包含TCP協議和IP協議,UDP(User Datagram Protocol)協議、ICMP(Internet Control Message Protocol) 協議和其他一些的協議的協議組
TCP/IP定義了電子設備(如計算機)如何連入因特網,以及數據如何在它們之間傳輸的標准.它是互聯網中的基本通信語言或協議,在私網中它也被用作通信協議,當用戶直接網絡連接時,計算機應提供一個TCP/IP程序的標准實現,而且接受所發送的信息的計算機也應只有一個TCP/IP程序的標准實現
TCP/IP協議並不完全符合OSI 標准定制的七層參考模型,它采取了四層的層級結構
網絡接口層:接收IP數據包並進行傳輸,從網絡上接收物理幀,抽取IP 轉交給下一層,對實際網絡的網絡媒體的管理,定義如何使用物理網絡 ,如以太網。
網際層IP: 負責提供基本的數據封包傳送功能,讓每一塊數據包都能打到目的主機,但不檢查是否被正確接收,主要表現為IP協議
傳輸層:在此層中,它提供了節點的數據傳送,應用程序之間的通信服務,主要是數據格式化,數據確認和丟失重傳等。主要協議包括TCP和UDP
應用層:應用程序間溝通單層,如萬維網(WWW)、簡單電子郵件傳輸(SMTP)、文件傳輸協議(FTP)、網絡遠程訪問協議(Telnet)等


fread和read的區別

read/write 操作文件描述符 (int型)
fread/fwrite 操作文件流 (FILE*型)
fread/fwrite 調用 read/write
read/write是系統調用,要自己分配緩存,也就是說效率要自己根據實際情況來控制。
fread/fwrite是標准輸入/輸出函數,不需要自己分配緩存,對於一般情況具有較高的效率。

什么是內存柵欄

內存柵欄(Memory Barrier)就是從本地或工作內存到主存之間的拷貝動作。
僅當寫操作線程先跨越內存柵欄而讀線程后跨越內存柵欄的情況下,寫操作線程所做的變更才對其他線程可見。關鍵字 synchronized 和 volatile 都強制規定了所有的變更必須全局可見,該特性有助於跨越內存邊界動作的發生,無論是有意為之還是無心插柳。
在程序運行過程中,所有的變更會先在寄存器或本地 cache 中完成,然后才會被拷貝到主存以跨越內存柵欄。此種跨越序列或順序稱為 happens-before。
寫操作必須要 happens-before 讀操作,即寫線程需要在所有讀線程跨越內存柵欄之前完成自己的跨越動作,其所做的變更才能對其他線程可見

TCP擁塞控制

擁塞控制的最終受控變量是發送端向網絡一次連續寫入的數據量(收到其中第一個數據報的確認之前),稱之為發送窗口(SWND),SWND 受接收方接受窗口(RWND)的影響。同時也受控於發送方的擁塞窗口(CWND)。SWND = min(RWND, CWND )

  • 當 cwnd < 慢開始門限(ssthresh) 時,使用慢開始算法
  • 當 cwnd > ssthresh 時,改用擁塞避免算法
  • 快重傳要求接收方在收到一個失序的報文段后就立即發出重復確認(為的是使發送方及早知道有報文段沒有到達對方)而不要等到自己發送數據時捎帶確認。快重傳算法規定,發送方只要一連收到三個重復確認就應當立即重傳對方尚未收到的報文段,而不必繼續等待設置的重傳計時器時間到期。快重傳配合使用的還有快恢復算法,是將 ssthresh減半,然后將 cwnd 設置為 ssthresh 的大小,之后執行擁塞避免算法

TCP為什么要三次握手、但是要四次揮手,握手第二步拆開行不行,揮手2 3步合並行不行,第三次握手確認的是什么能力

三次握手是為了避免僵屍連接,四次揮手是為了確保被斷開方的數據能夠全部完成傳輸,握手的第二部可以分開,不過需要增加一下狀態。如果服務端沒有數據要發送,揮手的2,3步可以合並,因為TCP是全雙工的。第三次握手確認的是客戶端時真實IP

TCP和UDP的區別

TCP 是面向連接的傳輸層協議,即傳輸數據之前必須先建立好連接, UDP 無連接
TCP 是點對點的兩點間服務,即一條 TCP 連接只能有兩個端點;UDP 支持一對一,一對多,多對一,多對多的交互通信
TCP 是可靠交付:無差錯,不丟失,不重復,按序到達;UDP 是盡最大努力交付,不保證可靠交付
TCP 有擁塞控制和流量控制保證數據傳輸的安全性;UDP 沒有擁塞控制,網絡擁塞不會影響源主機的發送效率
TCP 是動態報文長度,即 TCP 報文長度是根據接收方的窗口大小和當前網絡擁塞情況決定的。UDP 面向報文,不合並,不拆分,保留上面傳下來報文的邊界
TCP 首部開銷大,首部 20 個字節;UDP 首部開銷小,8 字節
如果數據完整性更重要,如文件傳輸、重要狀態的更新等,應該選用 TCP 協議。如果通信的實時性較重要,如視頻傳輸、實時通信等,則使用 UDP 協議

用udp會有什么問題

不可靠,不穩定
因為本身沒有重傳的控制機制,所以丟包率的可能是其最主要的問題

TCP是可靠的,為什么UDP還要去實現可靠連接

Linux查看網絡連接的命令

使用netstat查看存在的網絡連接
使用ping判斷主機間聯通情況

tcp可靠性傳輸怎么實現

序列號、確認應答、超時重傳
窗口控制與高速重發控制/快速重傳(重復確認應答)
擁塞控制
流量控制

虛函數的實現原理,繼承的時候怎么實現的

在有虛函數的類中,類的最開始部分是一個虛函數表的指針,這個指針指向一個虛函數表,表中放了虛函數的地址,實際的虛函數在代碼段(.text)中。當子類繼承了父類的時候也會繼承其虛函數表,當子類重寫父類中虛函數時候,會將其繼承到的虛函數表中的地址替換為重新寫的函數地址。使用了虛函數,會增加訪問內存開銷,降低效率。

局部變量分配在哪

分配在棧區

進程的棧有多大

32位Windows,一個進程棧的默認大小是1M,在vs的編譯屬性可以修改程序運行時進程的棧大小
inux下進程棧的默認大小是10M,可以通過 ulimit -s查看並修改默認棧大小
默認一個線程要預留1M左右的棧大小,所以進程中有N個線程時,Windows下大概有N M的棧大小
堆的大小理論上大概等於進程虛擬空間大小-內核虛擬內存大小。windows下,進程的高位2G留給內核,低位2G留給用戶,所以進程堆的大小小於2G。Linux下,進程的高位1G留給內核,低位3G留給用戶,所以進程堆大小小於3G

進程的最大線程數

32位windows下,一個進程空間4G,內核占2G,留給用戶只有2G,一個線程默認棧是1M,所以一個進程最大開2048個線程。當然內存不會完全拿來做線程的棧,所以最大線程數實際值要小於2048,大概2000個
32位Linux下,一個進程空間4G,內核占1G,用戶留3G,一個線程默認8M,所以最多380個左右線程(ps:ulimit -a 查看電腦的最大進程數,大概7000多個)

怎么快速把進程的棧用完

對函數進行遞歸調用
在函數中定義大對象

C++內存對齊

為什么要內存對齊:

  1. 平台原因(移植原因):不是所有的硬件平台都能訪問任意地址上的任意數據的;某些硬件平台只能在某些地址處取某些特定類型的數據,否則拋出硬件異常
  2. 性能原因:數據結構(尤其是棧)應該盡可能地在自然邊界上對齊。原因在於,為了訪問未對齊的內存,處理器需要作兩次內存訪問;而對齊的內存訪問僅需要一次訪問

如何進行內存對齊

  • 分配內存的順序是按照聲明的順序
  • 每個變量相對於起始位置的偏移量必須是該變量類型大小的整數倍,不是整數倍空出內存,直到偏移量是整數倍為止
  • 最后整個結構體的大小必須是里面變量類型最大值的整數倍
class A {
	int a, b;
	char c;
};

class B {
	int a, b;
	double c;
};

class C {
	char a;
	int b;
	double c;
};

class D {
	int a;
	double b;
	int c;
};

class E {
	int a;
	double b;
	int c;
	char d;
};
int main() {
	cout << sizeof(A) << " " << sizeof(B) << " " 
        << sizeof(C) << " " << sizeof(D) << " " << sizeof(E) << endl;
	return 0;
}
// output
// 12 16 16 24 24

引用和指針的區別?對const型常量可以取引用嗎

首先引用可以視作對象的別名,指針擁有自己的地址空間,其中保存着所指對象的地址
區別:

  1. 指針有自己的一塊空間,而引用只是一個別名
  2. 使用 sizeof 看一個指針的大小是 4,而引用則是被引用對象的大小
  3. 指針可以被初始化為 NULL,而引用必須被初始化且必須是一個已有對象的引用
  4. 作為參數傳遞時,指針需要被解引用才可以對對象進行操作,而直接對引用的修改都會改變引用所指向的對象
  5. 指針在使用中可以指向其它對象,但是引用只能是一個對象的引用,不能被改變
  6. 指針可以有多級指針(**p),而引用只有一級
  7. 指針和引用使用++運算符的意義不一樣
  8. 如果返回動態內存分配的對象或者內存,必須使用指針,引用可能引起內存泄露

http請求格式

HTTP 請求報文由請求行、請求頭部、空行 和 請求包體 4 個部分組成

get post的區別 put delete 知道嗎 put和post

http 1.X 2.0區別 ( 幀 流 推送 頭部壓縮 安全性等等

聯合索引:b+樹是什么狀態

HTTP頭部字段

詳細

http頭部可以包含二進制嗎

http2支持二進制,http1.x不支持

  • http 2 和 http 1 的區別
  1. HTTP2使用的是二進制傳送,HTTP1.X是文本(字符串)傳送。
    二進制傳送的單位是幀和流。幀組成了流,同時流還有流ID標示
  2. HTTP2支持多路復用
    因為有流ID,所以通過同一個http請求實現多個http請求傳輸變成了可能,可以通過流ID來標示究竟是哪個流從而定位到是哪個http請求
  3. HTTP2頭部壓縮
    HTTP2通過gzip和compress壓縮頭部然后再發送,同時客戶端和服務器端同時維護一張頭信息表,所有字段都記錄在這張表中,這樣后面每次傳輸只需要傳輸表里面的索引Id就行,通過索引ID查詢表頭的值
  4. HTTP2支持服務器推送
    HTTP2支持在未經客戶端許可的情況下,主動向客戶端推送內容

MYSQL的事務

ACID特性,原子性、一致性、隔離性、持久性

多級緩存的由來和使用

計算機結構中CPU和內存之間一般都配有一級緩存、二級緩存來增加交換速度,這樣當CPU調用大量數據時,就可避開內存直接從CPU緩存中調用,加快讀取速度。
根據CPU緩存得出多級緩存的特點:

  • 每一級緩存中儲存的是下一級緩存的一部分
  • 讀取速度按級別依次遞減,成本也依次遞減,容量依次遞增
  • 當前級別未命中時,才會去下一級尋找

項目文件傳輸時怎么限速

在客戶端進行文件傳輸時,每當上傳限制大小數據,就sleep一下

用戶態和內核態,為啥這樣做,好處是什么

用戶態和內核態是操作系統的兩種運行級別,兩者最大的區別就是特權級不同。用戶態擁有最低的特權級,內核態擁有較高的特權級。運行在用戶態的程序不能直接訪問操作系統內核數據結構和程序。內核態和用戶態之間的轉換方式主要包括:系統調用,異常和中斷進程

堆和棧的區別

堆是由低地址向高地址擴展;棧是由高地址向低地址擴展
堆中的內存需要手動申請和手動釋放;棧中內存是由 OS 自動申請和自動釋放,存放着參數、局部變量等內存
堆中頻繁調用 malloc 和 free,會產生內存碎片,降低程序效率;而棧由於其先進后出的特性,不會產生內存碎片
堆的分配效率較低,而棧的分配效率較高
棧是操作系統提供的數據結構,計算機底層對棧提供了一系列支持:分配專門的寄存器存儲棧的地址,壓棧和入棧有專門的指令執行;而堆是由 C/C++函數庫提供的,機制復雜,需要一些列分配內存、合並內存和釋放內存的算法,因此效率較低

五種IO模型

  1. 阻塞IO:調用者調用了某個函數,等待這個函數返回,期間什么也不做,不停的去檢查這個函數有沒有返回,必須等這個函數返回才能進行下一步動作
  2. 非阻塞IO:非阻塞等待,每隔一段時間就去檢測 IO 事件是否就緒。沒有就緒就可以做其他事
  3. 異步IO:
  4. 信號驅動IO:linux 用套接口進行信號驅動 IO,安裝一個信號處理函數,進程繼續運行並不阻塞,當 IO 時間就緒,進程收到 SIGIO 信號。然后處理 IO 事件
  5. 多路復用IO:linux 用 select/poll 函數實現 IO 復用模型,這兩個函數也會使進程阻塞,但是和阻塞 IO 所不同的是這兩個函數可以同時阻塞多個 IO 操作。而且可以同時對多個讀操作、寫操作的 IO 函數進行檢測。知道有數據可讀或可寫時,才真正調用 IO 操作函數

bio nio aio 區別

詳細
BIO (Blocking I/O):同步阻塞I/O模式,數據的讀取寫入必須阻塞在一個線程內等待其完成。這里使用那個經典的燒開水例子,這里假設一個燒開水的場景,有一排水壺在燒開水,BIO的工作模式就是, 叫一個線程停留在一個水壺那,直到這個水壺燒開,才去處理下一個水壺。但是實際上線程在等待水壺燒開的時間段什么都沒有做。
NIO (New I/O):同時支持阻塞與非阻塞模式,但這里我們以其同步非阻塞I/O模式來說明,那么什么叫做同步非阻塞?如果還拿燒開水來說,NIO的做法是叫一個線程不斷的輪詢每個水壺的狀態,看看是否有水壺的狀態發生了改變,從而進行下一步的操作。
AIO ( Asynchronous I/O):異步非阻塞I/O模型。異步非阻塞與同步非阻塞的區別在哪里?異步非阻塞無需一個線程去輪詢所有IO操作的狀態改變,在相應的狀態改變后,系統會通知對應的線程來處理。對應到燒開水中就是,為每個水壺上面裝了一個開關,水燒開之后,水壺會自動通知我水燒開了。

Select poll epoll

I/O 多路復用的特點是通過一種機制一個進程能同時等待多個文件描述符,而這些文件描述符(套接字描述符)其中的任意一個進入讀就緒狀態,select()函數就可以返回
I/O 多路復用和阻塞 I/O 其實並沒有太大的不同,事實上,還更差一些。因為這里需要使用兩個 system call (select 和 recvfrom),而 blocking IO 只調用了一個 system call (recvfrom)。但是,用 select 的優勢在於它可以同時處理多個 connection。
所以,如果處理的連接數不是很高的話,使用 select/epoll 的 web server 不一定比使用 multi-threading + blocking IO 的 web server 性能更好,可能延遲還更大。select/epoll 的優勢並不是對於單個連接能處理得更快,而是在於能處理更多的連接。

select
是最初解決 IO 阻塞問題的方法。用結構體 fd_set 來告訴內核監聽多個文件描述符,該結構體被稱為描述符集。由數組來維持哪些描述符被置位了。對結構體的操作封裝在三個宏定義中。通過輪尋來查找是否有描述符要被處理
存在的問題:

  1. 內置數組的形式使得 select 的最大文件數受限於 FD_SIZE;
  2. 每次調用 select 前都要重新初始化描述符集,將 fd 從用戶態拷貝到內核態,每次調用select 后,都需要將 fd 從內核態拷貝到用戶態
  3. 輪尋排查當文件描述符個數很多時,效率很低
  4. select 會修改傳入的參數數組,這個對於一個需要調用很多次的函數,是非常不友好的
  5. select 不是線程安全的,如果你把一個sock加入到select, 然后突然另外一個線程發現這個sock不用,要收回。對不起,這個select 不支持的,如果你要關掉這個sock, select的標准行為是不可預測的

poll
與 select 相比,poll 使用鏈表保存文件描述符,一沒有了監視文件數量的限制,但其他三個缺點依然存在

epoll
epoll 使用一個文件描述符管理多個描述符,將用戶關系的文件描述符的事件存放到內核的一個事件表中,這樣在用戶空間和內核空間的 copy 只需一次。Epoll 是事件觸發的,不是輪詢查詢的。沒有最大的並發連接限制,內存拷貝,利用 mmap() 文件映射內存加速與內核空間的消息傳遞。
epoll 對文件描述符的操作有兩種模式:LT(level trigger)和 ET(edge trigger
LT 模式是默認模式
LT(level triggered)是缺省的工作方式,並且同時支持 block 和 no-block socket.在這種做法中,內核告訴你一個文件描述符是否就緒了,然后你可以對這個就緒的 fd 進行 IO 操作。如果你不作任何操作,內核還是會繼續通知你的
ET 模式
ET(edge-triggered)是高速工作方式,只支持 no-block socket。在這種模式下,當描述符從未就緒變為就緒時,內核通過 epoll 告訴你。然后它會假設你知道文件描述符已經就緒,並且不會再為那個文件描述符發送更多的就緒通知,直到你做了某些操作導致那個文件描述符不再為就緒狀態了(比如,你在發送,接收或者接收請求,或者發送接收的數據少於一定量時導致了一個 EWOULDBLOCK 錯誤)。但是請注意,如果一直不對這個 fd 作 IO 操作(從而導致它再次變成未就緒),內核不會發送更多的通知(only once)
ET 模式在很大程度上減少了 epoll 事件被重復觸發的次數,因此效率要比 LT 模式高。epoll 工作在 ET 模式的時候,必須使用非阻塞套接口,以避免由於一個文件句柄的阻塞讀/阻塞寫操作把處理多個文件描述符的任務餓死
LT 模式與 ET 模式的區別如下:
LT 模式:當 epoll_wait 檢測到描述符事件發生並將此事件通知應用程序,應用程序可以不立即處理該事件。下次調用 epoll_wait 時,會再次響應應用程序並通知此事件。
ET 模式:當 epoll_wait 檢測到描述符事件發生並將此事件通知應用程序,應用程序必須立即處理該事件。如果不處理,下次調用 epoll_wait 時,不會再次響應應用程序並通知此事件。

epoll的底層實現

epoll發展於介紹
epoll中就緒列表引用着就緒的socket,所以它應能夠快速的插入數據。程序可能隨時調用epoll_ctl添加監視socket,也可能隨時刪除。當刪除時,若該socket已經存放在就緒列表中,它也應該被移除。所以就緒列表應是一種能夠快速插入和刪除的數據結構。雙向鏈表就是這樣一種數據結構,epoll使用雙向鏈表來實現就緒隊列。
既然epoll將“維護監視隊列”和“進程阻塞”分離,也意味着需要有個數據結構來保存監視的socket。至少要方便的添加和移除,還要便於搜索,以避免重復添加。紅黑樹是一種自平衡二叉查找樹,搜索、插入和刪除時間復雜度都是O(log(N)),效率較好。epoll使用了紅黑樹作為索引結構

Linux的阻塞和非阻塞怎么體現

阻塞(休眠)調用是沒有獲得資源則掛起進程,被掛起的進程進入休眠狀態,調用的函數只有在得到結果之后才返回,進程繼續。
非阻塞(休眠)是不能進行設備操作時不掛起,或返回,或反復查詢,直到可以進行操作為止,被調用的函數不會阻塞當前進程,而會立刻返回。

進程間通信

管道
具名管道
消息隊列
信號
信號量
共享內存
socket

進程上下文切換

1 保存當前進程的上下文
2 恢復某個先前被搶占的進程的上下文
3 將控制傳遞給這個新恢復的進程

詳細
保存處理器上下文環境即 PSW、PC 等寄存器和堆棧內容,保存到內核堆棧中
調整被中斷進程的 PCB 進程控制塊信息,改變進程狀態和其它信息
將進程控制塊移到相應隊列即阻塞隊列或就緒隊列
選擇另一個進程執行
更新所選擇進程的 PCB 進程控制塊
更新內存管理的數據結構
恢復第二個進程的上下文環境

中斷是什么

中斷定義:指當出現需要時,CPU暫時停止當前程序的執行轉而執行處理新情況的程序和執行過程。
硬件中斷是由外設引發的, 軟中斷是執行中斷指令產生的。
硬件中斷的中斷號是由中斷控制器提供的, 軟中斷的中斷號由指令直接指出, 無需使用中斷控制器。
硬件中斷是可屏蔽的, 軟中斷不可屏蔽。

中斷處理過程

  1. 中斷響應的事前准備
  2. CPU檢查是否有中斷/異常信號
  3. 根據中斷向量到IDT表中取得處理這個向量的中斷程序的段選擇符
  4. 根據取得的段選擇符到GDT中找相應的段描述符
  5. CPU根據特權級的判斷設定即將運行的中斷服務程序要使用的棧的地址
  6. 保護當前程序的現場
  7. 跳轉到中斷服務程序的第一條指令開始執行
  8. 中斷服務程序處理完畢,恢復執行先前中斷的程序

線程的上下文是什么、進程的上下文是什么

  • 線程上下文
    線程在切換的過程中需要保存當前線程 Id、線程狀態、堆棧、寄存器狀態等信息。其中寄存器主要包括 SP PC EAX 等寄存器,其主要功能如下
    SP:堆棧指針,指向當前棧的棧頂地址
    PC:程序計數器,存儲下一條將要執行的指令
    EAX:累加寄存器,用於加法乘法的缺省寄存器

  • 進程上下文
    進程上下文包括三個,用戶級上下文,寄存器上下文和系統級上下文
    用戶級上下文:指令,數據,共享內存、用戶棧
    寄存器上下文:程序計數器,通用寄存器,控制寄存器,狀態字寄存器,棧指針(用來指向用戶棧或者內存棧)
    系統級上下文:pcb,主存管理信息(頁表&段表)、核心棧

Linux文件系統,inode講一講?inode里存文件名稱嗎?

inode,中文名為索引結點,引進索引結點是為了在物理內存上找到文件塊,所以 inode 中包含文件的相關基本信息,比如文件位置、文件創建者、創建日期、文件大小等,輸入
stat 指令可以查看某個文件的 inode 信息
硬盤格式化的時候,操作系統自動將硬盤分成兩個區域,一個是數據區,一個是 inode 區,存放 inode 所包含的信息,查看每個硬盤分區的 inode 總數和已經使用的數量,可以用 df 命令
在 linux 系統中,系統內部並不是采用文件名查找文件,而是使用 inode 編號來識別文件。查找文件分為三個過程:系統找到這個文件名對應的inode 號碼,通過 inode 號碼獲得 inode 信息,根據 inode 信息找到文件數據所在的 block 讀取數據
除了文件名之外的所有文件信息,都存儲在 inode 之中

Linux chmod講一講,為啥有9位,分別對應什么

詳細
Linux chmod(英文全拼:change mode)命令是控制用戶對文件的權限的命令
Linux/Unix 的文件調用權限分為三級 : 文件所有者(Owner)、用戶組(Group)、其它用戶(Other Users)
9為代表三種用戶的權限: rwxrwxrwx,前三位rwx表示文件所有者的權限,中間三位表示用戶組的權限,最后三位表示其它用戶的權限

STL中的map和unordered_map的實現

map實現使用紅黑樹,unordered_map使用hash表
簡述

紅黑樹,B+樹,跳表

  • 紅黑樹。一種二叉查找樹,但在每個節點增加一個存儲位表示節點的顏色,可以是紅或黑(非紅即黑)。通過對任何一條從根到葉子的路徑上各個節點着色的方式的限制,紅黑樹確保沒有一條路徑會比其它路徑長出兩倍,因此,紅黑樹是一種弱平衡二叉樹(由於是弱平衡,可以看到,在相同的節點情況下,AVL樹的高度低於紅黑樹),相對於要求嚴格的AVL樹來說,它的旋轉次數少,所以對於搜索,插入,刪除操作較多的情況下,我們就用紅黑樹。
    性質:1. 每個節點非紅即黑 2. 根節點是黑的; 3. 每個葉節點(葉節點即樹尾端NULL指針或NULL節點)都是黑的; 4. 如果一個節點是紅的,那么它的兩兒子都是黑的; 5. 對於任意節點而言,其到葉子點樹NULL指針的每條路徑都包含相同數目的黑節點; 6. 每條路徑都包含相同的黑節點。

  • B+ 樹

  1. 有n棵子樹的非葉子結點中含有n個關鍵字(b樹是n-1個),這些關鍵字不保存數據,只用來索引,所有數據都保存在葉子節點(b樹是每個關鍵字都保存數據)。
  2. 所有的葉子結點中包含了全部關鍵字的信息,及指向含這些關鍵字記錄的指針,且葉子結點本身依關鍵字的大小自小而大順序鏈接(葉子節點組成一個鏈表)。
  3. 所有的非葉子結點可以看成是索引部分,結點中僅含其子樹中的最大(或最小)關鍵字。
  4. 通常在b+樹上有兩個頭指針,一個指向根結點,一個指向關鍵字最小的葉子結點。
  5. 同一個數字會在不同節點中重復出現,根節點的最大元素就是b+樹的最大元素。

怎么查看linux下哪個進程打開了哪些文件

詳細
lsof

虛擬地址怎么轉換成物理地址

對於段頁式系統來說,首先是查找段號,在對應段內找到頁號,在頁內找到頁內偏移,從而
程序地址:段號+頁號+頁內偏移

進程的調度算法

  • 先來先服務調度算法(FCFS)
    在進程調度中采用 FCFS 算法時,則每次調度是從就緒隊列中選擇一個最先進入該隊列的進程,為之分配處理機,使之投入運行,該進程一直運行到完或發生某事件而阻塞后才放棄處理機。–非搶占式
    有利於長作業,不利於短作業;有利於 CPU 繁忙的作業,不利於 I/O 繁忙的作業
  • 短作業(進程)優先調度算法(SJF、SPF)
    短進程(SPF)調度算法則是從就緒隊列中選出一個估計運行時間最短的進程,將處理機分配給它,使它立即執行並一直執行到完成,或發生某事件被阻塞放棄處理機。—非搶占式
    比 FCFS 改善平均周轉時間和平均帶權周轉時間,提高系統吞吐量;對長作業不利,沒能根據緊迫程度來划分執行的優先級,難以准確估計 作業或進程的執行時間。
  • 優先權調度算法
    當該算法用於進程調度時,將把處理機分配給就緒進程隊列中優先級最高的進程投入運行。分為非搶占式優先級算法和搶占式優先級算法。
  • 時間片輪轉調度算法
    系統將就緒進程按到達的順序排成一個隊列,按 FCFS 原則,進程調度程序總是選擇就緒隊列中的第一個進程執行,且只運行一個時間片。時間用完后,即使此進程並未完成,仍然將處理機分配給下一個就緒的進程,將此進程返回到就緒隊列的末尾,等候重新運行
  • 多級反饋隊列調度算法
    設置 n 個就緒隊列,優先級從 1 到 n 依次遞減,即第 1 級隊列優先級最高每個隊列的時間也不相同,優先級高的隊列,時間片越短,即從 1 到 n 時間片越來越多。一個新進程進入內存后,先插入第一級隊列的末尾,按照FCFS的原則等待調度。如果某個進程可在一個時間片內完成,那么結束此進程;如果某進程在一個時間片內無法完成,就把此進程轉入下一級隊列的末尾,按照 FCFS 原則等待調度,一直到第 n-1 級隊列。當一個很長的進程從第 1 級一直到第 n 級隊列,那么它在第 n 級隊列按照時間片輪轉的方式等待調度。僅當第 1 級隊列為空時,調度程序才調度第 2 級隊列中的進程執行,依次類推。如果處理機正在處理第 i 級的隊列的某進程,又有新的進程進入優先級更高的隊列(第 1——i -1),則此時新的進程搶占處理機,原本正在執行第 i 級此進程停止運行,放到第 i 級就緒隊列的末尾,把處理機分配給更高優先級的進程
    短作業優先、短批處理作業周轉時間較短、長批處理作業不會長期得不到執行

怎么解鎖死鎖

死鎖四條件:

  1. 互斥
  2. 不可搶奪
  3. 占有與等待
  4. 循環等待

解決死鎖的方案:

  • 允許進程強行從占有者那里奪取某些資源,破壞不可搶占條件
  • 進程在運行前一次性地向系統申請它所需要的全部資源,破壞了保持與等待條件
  • 把資源事先分類編號,按號分配,使進程在申請,占用資源時不會形成環路,破壞了循環等待條件

死鎖避免:銀行家算法

OSI七層模型

  • 物理層:規定通信設備的機械的、電氣的、功能的和過程的特性,用以建立、維護和拆除物理鏈路連接。在這一層,數據的單位稱為比特(bit)。屬於物理層定義的典型規范代表包括:EIA/TIA RS-232、EIA/TIA RS-449、V.35、RJ-45 等
  • 數據鏈路層:在物理層提供比特流服務的基礎上,建立相鄰結點之間的數據鏈路,通過差
    錯控制提供數據幀(Frame)在信道上無差錯的傳輸,並進行各電路上的動作系列。數據鏈路層在不可靠的物理介質上提供可靠的傳輸。該層的作用包括:物理地址尋址、數據的成幀、流量控制、數據的檢錯、重發等。在這一層,數據的單位稱為幀(frame)。數據鏈路層協議的代表包括:SDLC、HDLC、PPP、STP、幀中繼等。
  • 網絡層:在 計算機網絡中進行通信的兩個計算機之間可能會經過很多個數據鏈路,也可能還要經過很多通信子網。網絡層的任務就是選擇合適的網間路由和交換結點,確保數據及時傳送。網絡層將數據鏈路層提供的幀組成數據包,包中封裝有網絡層包頭,其中含有邏輯地址信息- -源站點和目的站點地址的網絡地址。IP 是第 3 層問題的一部分,此外還有一些路由協議和地址解析協議(ARP)。有關路由的一切事情都在這第 3 層處理。地址解析和路由是 3 層的重要目的。網絡層還可以實現擁塞控制、網際互連等功能。在這一層,數據的單位稱為數據包(packet)。網絡層協議的代表包括:IP、IPX、RIP、OSPF 等。
  • 傳輸層:第 4 層的數據單元也稱作數據包(packets)。但是,當你談論 TCP 等具體的協議時又有特殊的叫法,TCP 的數據單元稱為段 (segments)而 UDP 協議的數據單元稱為“數據報(datagrams)”。這個層負責獲取全部信息,因此,它必須跟蹤數據單元碎片、亂序到達的數據包和其它在傳輸過程中可能發生的危險。第 4 層為上層提供端到端(最終用戶到最終用戶)的透明的、可靠的數據傳輸服務。所謂透明的傳輸是指在通信過程中 傳輸層對上層屏蔽了通信傳輸系統的具體細節。傳輸層協議的代表包括:TCP、UDP、SPX 等。
  • 會話層:在會話層及以上的高層次中,數據傳送的單位不再另外命名,而是統稱為報文。會話層不參與具體的傳輸,它提供包括訪問驗證和會話管理在內的建立和維護應用之間通信的機制。如服務器驗證用戶登錄便是由會話層完成的。
  • 表示層:這一層主要解決擁護信息的語法表示問題。它將欲交換的數據從適合於某一用戶的抽象語法,轉換為適合於 OSI 系統內部使用的傳送語法。即提供格式化的表示和轉換數據服務。數據的壓縮和解壓縮, 加密和解密等工作都由表示層負責。
  • 應用層:為操作系統或網絡應用程序提供訪問網絡服務的接口。應用層協議的代表包括:Telnet、FTP、HTTP、SNMP 等

ARP協議

ARP(地址解析)協議是一種解析協議,本來主機是完全不知道這個 IP 對應的是哪個主機的哪個接口,當主機要發送一個 IP 包的時候,會首先查一下自己的 ARP 高速緩存表(最近數據傳遞更新的 IP-MAC 地址對應表),如果查詢的 IP-MAC 值對不存在,那么主機就向網絡廣播一個 ARP 請求包,這個包里面就有待查詢的 IP 地址,而直接收到這份廣播的包的所有主機都會查詢自己的 IP 地址,如果收到廣播包的某一個主機發現自己符合條件,那么就回應一個 ARP 應答包(將自己對應的 IP-MAC 對應地址發回主機),源主機拿到 ARP 應答包后會更新自己的 ARP 緩存表。源主機根據新的 ARP 緩存表准備好數據鏈路層的的數據包發送工作

malloc底層的實現

Malloc 函數用於動態分配內存。為了減少內存碎片和系統調用的開銷,malloc 其采用內存池的方式,先申請大塊內存作為堆區,然后將堆區分為多個內存塊,以塊作為內存管理的基本單位。當用戶申請內存時,直接從堆區分配一塊合適的空閑塊。Malloc 采用隱式鏈表結構將堆區分成連續的、大小不一的塊,包含已分配塊和未分配塊;同時 malloc 采用顯示鏈表結構來管理所有的空閑塊,即使用一個雙向鏈表將空閑塊連接起來,每一個空閑塊記錄了一個連續的、未分配的地址
當進行內存分配時,Malloc 會通過隱式鏈表遍歷所有的空閑塊,選擇滿足要求的塊進行分配;當進行內存合並時,malloc 采用邊界標記法,根據每個塊的前后塊是否已經分配來決定是否進行塊合並
Malloc 在申請內存時,一般會通過 brk 或者 mmap 系統調用進行申請。其中當申請內存小於128K 時,會使用系統函數 brk 在堆區中分配;而當申請內存大於 128K 時,會使用系統函數 mmap在映射區分配

邏輯地址---(分段硬件)>>> 線型地址 --- (分頁硬件)>>> 物理地址 的過程,虛擬內存的實現

為什么引入虛擬內存

為了防止不同進程同一時刻在物理內存中運行而對物理內存的爭奪和踐踏,采用了虛擬內存。

虛擬內存技術使得不同進程在運行過程中,它所看到的是自己獨自占有了當前系統的 4G 內存。所有進程共享同一物理內存,每個進程只把自己目前需要的虛擬內存空間映射並存儲到物理內存上。

虛擬內存的好處:

  1. 擴大地址空間;
  2. 內存保護:每個進程運行在各自的虛擬內存地址空間,互相不能干擾對方。虛存還對特定的內存地址提供寫保護,可以防止代碼或數據被惡意篡改。
  3. 公平內存分配。采用了虛存之后,每個進程都相當於有同樣大小的虛存空間。
  4. 當進程通信時,可采用虛存共享的方式實現。
  5. 當不同的進程使用同樣的代碼時,比如庫文件中的代碼,物理內存中可以只存儲一份這樣的代碼,不同的進程只需要把自己的虛擬內存映射過去就可以了,節省內存
  6. 虛擬內存很適合在多道程序設計系統中使用,許多程序的片段同時保存在內存中。當一個程序等待它的一部分讀入內存時,可以把 CPU 交給另一個進程使用。在內存中可以保留多個進程,系統並發度提高
  7. 在程序需要分配連續的內存空間的時候,只需要在虛擬內存空間分配連續空間,而不需要實際物理內存的連續空間,可以利用碎片

虛擬內存的代價:

  1. 虛存的管理需要建立很多數據結構,這些數據結構要占用額外的內存
  2. 虛擬地址到物理地址的轉換,增加了指令的執行時間。
  3. 頁面的換入換出需要磁盤 I/O,這是很耗時的
  4. 如果一頁中只有一部分數據,會浪費內存。

64位操作系統下,實現一個鏈接式的hash map,保存n組(key, value) 對,假設key, value各占8字節,問一共需要占多少字節

64位操作系統,指針8字節,假設hash函數有m個值,則8*m + (8+8+8)*n = 8m + 24n
8m表示開始的m個指針大小,(8+8+8)分別表示指針、key和value

延時隊列怎么實現

詳細
什么是延時隊列?顧名思義:首先它要具有隊列的特性,再給它附加一個延遲消費隊列消息的功能,也就是說可以指定隊列中的消息在哪個時間點被消費。
對於C++來說,可以直接使用優先隊列,如在隊列中存儲消息id以及過期時間,隊列自動按照時間排序,每次從隊列中拿第一個消息進行消費。

怎么解決緩存擊穿?怎么解決緩存雪崩?

  • 緩存穿透

緩存穿透是指緩存和數據庫中都沒有的數據,而用戶不斷發起請求,如發起為id為“-1”的數據或id為特別大且不存在的數據。這時的用戶很可能是攻擊者,攻擊會導致數據庫壓力過大。
解決方案:

  1. 接口層增加校驗,如用戶鑒權校驗,id做基礎校驗,id<=0的直接攔截;
    從緩存取不到的數據,在數據庫中也沒有取到,這時也可以將key-value對寫為key-null,緩存有效時間可以設置短點,如30秒(設置太長會導致正常情況也沒法使用)。這樣可以防止攻擊用戶反復用同一個id暴力攻擊
  • 緩存擊穿

緩存擊穿是指緩存中沒有但數據庫中有的數據(一般是緩存時間到期),這時由於並發用戶特別多,同時讀緩存沒讀到數據,又同時去數據庫去取數據,引起數據庫壓力瞬間增大,造成過大壓力
解決方案:
設置熱點數據永遠不過期
加互斥鎖降低從數據庫中讀取數據頻率

  • 緩存雪崩

緩存雪崩是指緩存中數據大批量到過期時間,而查詢數據量巨大,引起數據庫壓力過大甚至down機。和緩存擊穿不同的是,緩存擊穿指並發查同一條數據,緩存雪崩是不同數據都過期了,很多數據都查不到從而查數據庫
解決方案:

  1. 緩存數據的過期時間設置隨機,防止同一時間大量數據過期現象發生。
  2. 如果緩存數據庫是分布式部署,將熱點數據均勻分布在不同搞得緩存數據庫中。
  3. 設置熱點數據永遠不過期。

10M帶寬,下載速度大約有多少

10 M 帶寬單位是bps,而我們常使用的是Byte,因此約為10/8=1.25M。上行/上傳速度會更慢,大約只有256K

客戶端和服務端建立socket連接的過程,相關的方法名

  • 服務端
  1. 創建一個socket,用函數socket();
  2. 設置socket屬性,用函數setsockopt(); * 可選
  3. 綁定IP地址、端口等信息到socket上,用函數bind();
  4. 開啟監聽,用函數listen();
  5. 接收客戶端上來的連接,用函數accept();
  6. 收發數據,用函數send()和recv(),或者read()和write();
  7. 關閉網絡連接;
  8. 關閉監聽;
    closesocket
  • 客戶端
  1. 創建一個socket,用函數socket();
  2. 設置socket屬性,用函數setsockopt();* 可選
  3. 綁定IP地址、端口等信息到socket上,用函數bind();* 可選
  4. 設置要連接的對方的IP地址和端口等屬性;
  5. 連接服務器,用函數connect();
  6. 收發數據,用函數send()和recv(),或者read()和write();
  7. 關閉網絡連接;

路由器和交換機有什么區別,分別工作在哪一層

交換機,工作在 OSI 第二層(數據鏈路層),根據 MAC 地址進行數據轉發。
路由器,工作在 OSI 第三次(網絡層),根據 IP 進行尋址轉發數據包

fork 與 vfork

fork與vfork都是創建進程,vfork創建的子進程是與父進程共享地址空間,而fork創建的子進程是父進程的副本,它們的區別如下

  1. fork:子進程拷貝父進程的數據段,代碼段
  2. vfork:子進程與父進程共享數據段
  3. fork:父子進程的執行次序不確定
  4. vfork 保證子進程先運行,在調用exec 或exit 之前與父進程數據是共享的,在它調用exec或exit 之后父進程才可能被調度運行。如果在調用這兩個函數之前子進程依賴於父進程的進一步動作,則會導致死鎖。

vfork為什么需要exit()而不用return

如果你在vfork中return了,那么,這就意味main()函數return了,注意因為函數棧父子進程共享,所以整個程序的棧就跪了。

#include<unistd.h>
#include<iostream>
#include<stdlib.h>

using namespace std;

int gdata = 1;

int main(){
    pid_t pid;
    pid = fork(); //更改為vfork
    int tmp = 10;
    if(pid == -1) cout << "fork error"<< endl;
    else if(pid == 0){
        cout << "son process, my parent is: " << getppid() << endl;
        gdata = 11;
        cout << "in son: " << gdata << endl;
        tmp = 100;
        cout << "son tmp: " << tmp << endl;
        exit(0);
    }else {
        cout << "parent process, my pid is " << getpid() << endl;
        cout << "in parent: " << gdata << endl;
        cout << "in parent: " << tmp << endl;
    }
    return 0;
}
/*
當前輸出:

son process, my parent is: 9743
in son: 11
son tmp: 100
parent process, my pid is 9743
in parent: 1
in parent: 10

將fork改為vfork

son process, my parent is: 9735
in son: 11
son tmp: 100
parent process, my pid is 9735
in parent: 11
in parent: 10
*/

客戶端怎么校驗https的證書是否合法

詳細
數字證書包含以下信息:申請者公鑰、申請者的組織信息和個人信息、簽發機構 CA 的信息、有效時間、證書序列號等信息的明文,同時包含一個簽名
客戶端在對服務器say hello之后,服務器將公開密鑰證書發送給客戶端,注意這個證書里面包含了公鑰+各種信息+簽名(私鑰對各種信息加密后生成簽名),客戶端收到公開密鑰證書后,相當於收到了一個包裹里面有公鑰+各種信息+簽名,怎么樣使用這三個數據來校驗呢,很簡單,公鑰加密,私鑰解,私鑰加密公鑰也可以解,只要利用公鑰對簽名進行解密,然后最和各種信息做比較就可以校驗出證書的合法性。

兩個進程某變量用gdb調試打印出的地址是否會一樣

如果使用指針訪問一個區域,指針+1 、指針-1可能會訪問到什么?為什么

一致性哈希

詳細

通過hash環來實現負載均衡,將不同的服務器hash映射到一致性hash環上,當服務請求到來時,使用hash將其映射到hash環上,然后可以采用如順時針尋找的方法選擇距其最近的服務器進行服務。
當服務器較少或hash公式不夠好時,可能出現大多數請求都會落在同一個服務器上,這就是數據傾斜,可以采用添加服務器、虛擬節點、更換一致性hash的方法進行解決。

Tcp: 拔網線之后連接是否存在 為什么 (記得tcp的長連接是有一個類似心跳檢測的機制,忘了叫啥了,面試官問我心跳檢測是在傳輸層嗎還是應用層 ,我說應用層有心跳檢測,但tcp那層也有類似的,后來回來看了下tcp的保活,

操作系統如何識別tcp連接

C++鎖

互斥鎖(Mutex) -- 互斥鎖用於控制多個線程對他們之間共享資源互斥訪問的一個信號量。
條件鎖 -- 條件鎖就是所謂的條件變量
自旋鎖
讀寫鎖

vector、stack、queue這些容器是怎么實現的

stack 底層一般用 deque 實現,封閉頭部即可,不用 vector 的原因應該是容量大小有限制,擴容耗時
queue 底層一般用 deque 實現,封閉頭部的出口和前端的入口即可
priority_queue 的底層數據結構一般為 vector 為底層容器,堆 heap 為處理規則來管理底層容器實現

並發編程三要素

原子性:即一個操作或者多個操作 要么全部執行並且執行的過程不會被任何因素打斷,要么就都不執行
可見性:指當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值。
有序性:即程序執行的順序按照代碼的先后順序執行,首先什么是指令重排序,一般來說,處理器為了提高程序運行效率,可能會對輸入代碼進行優化,它不保證程序中各個語句的執行先后順序同代碼中的順序一致,但是它會保證程序最終執行結果和代碼順序執行的結果是一致的。處理器在進行重排序時也是會考慮指令之間的數據依賴性。指令重排序不會影響單個線程的執行,但是會影響到線程並發執行的正確性。

短網址服務 -- 將很長的網址連接設計成短網址

  • 使用分布式 ID 生成器
  • 使用mysql數據庫存儲id與網址的對應關系
  • 使用302重定向

如果用了 301,Google、百度等搜索引擎,搜索的時候會直接展示真實地址,那我們就無法統計到短地址被點擊的次數了,也無法收集用戶的 Cookie、User Agent 等信息,這些信息可以用來做很多有意思的大數據分析,也是短網址服務商的主要盈利來源
詳細

零拷貝

“零拷貝”:在整個發送數據過程中,數據的復制是必不可少的,這里數據復制分兩種類型,一種是CPU參與的一個字節一個字節處理的數據復制,一個是CPU不用參與,通過專有硬件DMA參與的,批量數據復制。自然,不用CPU參與的數據復制性能高。而“零拷貝”所說的拷貝,其實指的是,減少CPU參與的數據拷貝,最好減少到零次,但是各種實現方式里,很多種都只是減少一次兩次,並沒有直接讓CPU參與的數據復制數歸零

通過mmap實現的零拷貝I/O

發出mmap系統調用,導致用戶空間到內核空間的上下文切換(第一次上下文切換)。通過DMA引擎將磁盤文件中的內容拷貝到內核空間緩沖區中(第一次拷貝: hard drive ——> kernel buffer)。
mmap系統調用返回,導致內核空間到用戶空間的上下文切換(第二次上下文切換)。接着用戶空間和內核空間共享這個緩沖區,而不需要將數據從內核空間拷貝到用戶空間。因為用戶空間和內核空間共享了這個緩沖區數據,所以用戶空間就可以像在操作自己緩沖區中數據一般操作這個由內核空間共享的緩沖區數據
發出write系統調用,導致用戶空間到內核空間的上下文切換(第三次上下文切換)。將數據從內核空間緩沖區拷貝到內核空間socket相關聯的緩沖區(第二次拷貝: kernel buffer ——> socket buffer)。
write系統調用返回,導致內核空間到用戶空間的上下文切換(第四次上下文切換)。通過DMA引擎將內核空間socket緩沖區中的數據傳遞到協議引擎(第三次拷貝: socket buffer ——> protocol engine
通過mmap實現的零拷貝I/O進行了4次用戶空間與內核空間的上下文切換,以及3次數據拷貝。其中3次數據拷貝中包括了2次DMA拷貝和1次CPU拷貝

快照讀在讀提交和可重復讀(RR和RC)模式下的問題

事務總能夠讀取到,自己寫入(update /insert /delete)的行記錄
RC下,快照讀總是能讀到最新的行數據快照,當然,必須是已提交事務寫入的
RR下,某個事務首次read記錄的時間為T,未來不會讀取到T時間之后已提交事務寫入的記錄,以保證連續相同的read讀到相同的結果集
在RC級別下每次都是讀取最新的快照版本,在RR級別下是事務開啟時生成一個全局快照,后續的快照讀都讀取這個快照

索引失效

1.有or必全有索引;
2.復合索引未用左列字段;
3.like以%開頭;
4.需要類型轉換;
5.where中索引列有運算;
6.where中索引列使用了函數;
7.如果mysql覺得全表掃描更快時(數據少);

內存泄漏

內存泄漏是指由於疏忽或錯誤造成了程序未能釋放掉不再使用的內存的情況。內存泄漏並非指內存在物理上消失,而是應用程序分配某段內存后,由於設計錯誤,失去了對該段內存的控制。

檢查、定位內存泄漏:

檢查方法:
在 main 函數最后面一行,加上一句_CrtDumpMemoryLeaks()。調試程序,自然關閉程序讓其退出,查看輸出:
被{}包圍的 數字x 就是我們需要的內存泄漏定位值
定位代碼位置:
在 main 函數第一行加上_CrtSetBreakAlloc(x);意思就是在申請 x這塊內存的位置中斷。然后調試程序,程序中斷了,查看調用堆棧。加上頭文件#include <crtdbg.h>

算法題

  1. 給定一些數組,例如下面的格式,他們都表示一個區間,然后你需要將區間進行合並 [1,2],[2,4],[3,7],[8,11] 如上所示, [1,2] 和 [2,4] = [1,4] ,然后 [1,4] 和 [3,7] = [1,7],最后 [1,7] 和 [8,11] 無法合並,所以最后結果應該返回 [1,7],[8,11]

先按照各個區間的開始位置排序,然后進行合並

  1. 數組出現次數最多的TOP N,給定一個數組,例如 [1,1,2,2,2,3,3,3,3]這樣的,里面的數組不一定連續並且有序,假設我輸入 2,這個2表示出現次數最高的兩個, 那么你需要給我返回 2,3

使用 map 存儲各個數字與它們出現的次數的數據對,使用大小為 N 的優先隊列保持TOP N個數字

  1. 鏈表的兩兩翻轉, 給定鏈表: 1->2->3->4->5->6->7 返回結果: 2->1->4->3->6->5->7

三個指針即可

  1. 兩個有序數組合並 -- 多個有序數組的合並

多指針合並

  1. 二叉樹中兩個節點的最遠距離 *
int FathestDistan2(TreeNode* pRoot, int &MaxValue)
{
	if (pRoot == NULL)
		return 0;
	int LeftOfHeight = FathestDistan2(pRoot->pLeft, MaxValue);
	int RightOfHeight = FathestDistan2(pRoot->pRight, MaxValue);
 
	if (LeftOfHeight + RightOfHeight > MaxValue)
		MaxValue = LeftOfHeight + RightOfHeight;
 
	return LeftOfHeight > RightOfHeight ? LeftOfHeight + 1 : RightOfHeight + 1;
}
  1. 寫sql語句 課程A分數大於平均分的總人數
select count(*) from test where score > (select avg(score) from test where name="a") and name ="a";
  1. 二維數組找單詞

dfs

  1. top K 問題

優先隊列

  1. 反轉鏈表的n,m區間

三指針

  1. 二叉樹里面兩個節點的最近公共父節點

dfs,求左,求右,與求是否與當前節點重合

  1. A-Z的全排列

dfs

  1. 36進制加法
  2. 二叉搜索樹,寫節點的刪除代碼
  3. 最大連續子序列和

貪心

  1. 最小堆建堆 (下堆排序)
  2. 多個串,將含有相同字母的串放到同一個集合,返回集合向量

將字符串與排序的字符串映射,所以一個排序字符串的映射屬於同一個集合

  1. 給一組日志文件,包含用戶id,登陸時間,登出時間,假定時間范圍一天之內,求出一天之內在線用戶數量的峰值,並給出時間區間

把登陸時間和登出時間分別當做兩個向量分別排序,然后從小到大掃描,類似歸並排序的merge操作,先判斷下一次操作登陸在前還是登出在前,登陸加一登出減一,得到當前時刻的在線人數

#include<iostream>
#include<vector>
#include<numeric>
#include<math.h>
#include<string>
#include<map>
#include<set>
#include<algorithm>
#include<queue>
#define pii pair<int, int>
using namespace std;

void max_num_range(vector<pair<int, int>>& vpii){
    priority_queue<int, vector<int>, greater<int>> pq1, pq2;
    for(pair<int, int> item: vpii){
        pq1.emplace(item.first);
        pq2.emplace(item.second);
    }
    int c_on = 0, max_on = 0, begin_time=0, end_time=0;
    while(pq1.size() && pq2.size()){
        if(pq1.top() <= pq2.top()){
            ++c_on;
            if(c_on > max_on){
                max_on = c_on;
                begin_time = pq1.top();
            }
            pq1.pop();
        }else{
            --c_on;
            if(c_on == max_on-1) end_time = pq2.top();
            pq2.pop();
        }
    }
    if(end_time == 0) end_time = pq2.top();
    cout << "最大在線人數:" << max_on << "\n開始時間: " << begin_time << "\n結束時間: " << end_time << endl;
}

int main(){
    vector<pair<int, int>> vpii;
    pii a({1,10}), b({2,12}), c({3,9}), d({4,6}), e({5,6});
    vpii.emplace_back(a);
    vpii.emplace_back(b);
    vpii.emplace_back(c);
    vpii.emplace_back(d);
    vpii.emplace_back(e);
    max_num_range(vpii);
    return 0;
}
  1. 給一個01矩陣,計算1被0分割成了幾塊

dfs

  1. 在一個表盤上,從0時刻出發,一次移動一格,順時針逆時針均可,n步回到0時刻,問有多少種不同的路徑

動態規划,0初始化向量,dp[i] = dp[i-1]+dp[i+1]

  1. 手寫一個LRU算法談談思路,要求 put 和 get 操作的時間復雜度均為 O(1)

map,雙向鏈表

  1. 求二叉樹深度

層序遍歷

  1. 求第一個不連續的數,題目的意思是給定一個數組,比如[8,1,4,5,2,7],這個數組排序后是1 2 4 5 7 8,那么第一個不連續的數就是4。要求時間復雜度O(n)

找min和max,建立vector,兩次遍歷即可

  1. 二叉樹的中序遍歷
  2. 最長公共子序列

動態規划 dp[i][j] = dp[i-1][j-1] || dp[i][j] = max(dp[i-1][j], dp[i][j-1])

  1. SQL查找第二高工資
select * from test where score < (select max(score) from test order by score desc limit 2) order by score desc limit 1;
select * from test where name = 'a' order by score desc limit 1,1;
  1. leetcode39 組合總數

dfs

  1. leetcode7 整數反轉
  2. 有一組數據, 2個1, 2個2,2個3, 2個4,。。。n個n 寫程序找到這樣一種排列, 使得 2個1之間1個數字,2個2之間2個數字,2個3之間3個數字,2個4之間4個數字,。。。2個n之間n個數字 例如n=4時, 41312432
  1. 最長上升子序列
class Solution {
public:
    /**
     * retrun the longest increasing subsequence
     * @param arr int整型vector the array
     * @return int整型vector
     */
    vector<int> LIS(vector<int>& arr) {
        // write code here
        int n = arr.size();
        vector<int> tail(n); //貪心的希望末尾的數字越小越好
        vector<int> curLen(n); //arr的第i個元素對應的最大遞增子序列長度
        int ans = 0;
        for(int i = 0; i < n; i ++){
            int left = 0, right = ans;
            while(left < right){ //找第一個大於等於arr[i]的位置
                int mid = left + (right - left) / 2;
                if(tail[mid] >= arr[i]) right = mid;
                else left = mid + 1;
            }
            tail[left] = arr[i];
            curLen[i] = left + 1; //當前元素對應最大遞增子序列的長度
            if(left == ans) ans ++;
        }
        vector<int> res(ans);
        for(int i = arr.size() - 1, j = ans; i >= 0; i --){
            if(curLen[i] == j) res[-- j] = arr[i];
        }
        return res;
    }
};
  1. 輸入字符串算式,里面有加減乘除和小數,計算結果
  1. leetcode322 零錢兌換
class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        if(coins.empty()) return -1;
        if(amount == 0) return 0;
        vector<int> vec(amount+1, INT_MAX-10);
        vec[0] = 0;
        for(int item: coins){
            for(int i = 1;i <= amount;++i){
                if(item == i) vec[i] = 1;
                else if(item < i){
                    vec[i] = min(vec[i], vec[i-item]+1);
                }
            }
        }
        return vec[amount] == INT_MAX-10 ? -1 : vec[amount];
    }
};
  1. 二叉樹找第k大怎么做,空間復雜度多少
  2. 二叉搜索樹找第k大怎么做,空間復雜度多少

中序遍歷+計數

  1. 用快排做topk怎么做,時間復雜度怎么算
void quick(vector<int>& vec, int l_index, int r_index){
    // 快排 快速排序
    if(l_index >= r_index) return;
    int mid_index = (l_index+r_index)/2;
    int key = vec[mid_index], i, j;
    for(i = l_index, j = r_index; i < j;){
        for(;i <= j && vec[i] < key;++i);
        for(;j >= i && vec[j] >= key; --j);
        if(i < j) swap(vec[i], vec[j]);
    }
    if(i < mid_index && vec[i] >= vec[mid_index]) 
        swap(vec[mid_index], vec[i]);
    quick(vec, l_index, i-1);
    quick(vec, i+1, r_index);
}

// 快排變形.cpp : 此文件包含 "main" 函數。程序執行將在此處開始並結束。
//

#include <iostream>
#include <vector>

using namespace std;

int partion(vector<int>& nums,int left, int right)
{
    int key = nums[left];
    while (left < right)
    {
        while (left < right && nums[right] >= key) right--;
        nums[left] = nums[right];
        while (left < right && nums[left] <= key) left++;
        nums[right] = nums[left];
    }
    nums[left] = key;
    return left;
}

int topK(vector<int>& nums, int low, int high, int k)
{
    if (low == high) return nums[low];
    int pos = partion(nums, low, high);

    int len = high - pos + 1;
    if (len == k) return nums[pos];
    else if (len > k) return topK(nums, pos+1, high, k);
    else return topK(nums, low, pos -1, k-len);
}


int main()
{
    vector<int> nums = { 12,52,1,59,46,49,65,58,15,34,28,9,5 };
    int n = nums.size();
    cout << topK(nums, 0, n - 1, 3);
}
  1. 實現一個循環隊列,並設計一個O(1)的算法獲取最大值

考慮數組+雙指針+優先級隊列

  1. 鏈表 奇位上升偶位下降 整合成升序鏈表
  2. LRU實現、插入操作、 描述數據結構如何變化 (說雙向鏈表加哈希,在雙向鏈表上做lru,加哈希表是為了快速定位要移動的節點)
  3. 實現哈希表 沖突過多的時候如何解決 (擴容
  4. 大數加法 鏈表
  5. 尋找中位數
  6. 堆排序
void adjust(vector<int> &arr, int len, int index)
{
    int left = 2*index + 1; // index的左子節點
    int right = 2*index + 2;// index的右子節點

    int maxIdx = index;
    if(left<len && arr[left] > arr[maxIdx])     maxIdx = left;
    if(right<len && arr[right] > arr[maxIdx])     maxIdx = right;

    if(maxIdx != index)
    {
        swap(arr[maxIdx], arr[index]);
        adjust(arr, len, maxIdx);
    }

}

// 堆排序
void heapSort(vector<int> &arr, int size)
{
    // 構建大根堆(從最后一個非葉子節點向上)
    for(int i=size/2 - 1; i >= 0; i--)
    {
        adjust(arr, size, i);
    }

    // 調整大根堆
    for(int i = size - 1; i >= 1; i--)
    {
        swap(arr[0], arr[i]);           // 將當前最大的放置到數組末尾
        adjust(arr, i, 0);              // 將未完成排序的部分繼續進行堆排序
    }
}

int main()
{
    vector<int> arr = {8, 1, 14, 3, 21, 5, 7, 10};
    heapSort(arr, arr.size());
    for(int i=0;i<arr.size();i++)
    {
        cout<<arr[i]<<" ";
    }
    return 0;
}
  1. 線程的創建與使用
#include<mutex>
#include<windows.h>
#define pii pair<int, int>
using namespace std;

mutex g_mutex;

void print123()
{
	//g_mutex.lock();
	for (int i = 0; i < 3; i++) {
		this_thread::sleep_for(chrono::milliseconds(100));
		cout << i + 1;
	}
	//g_mutex.unlock();
}

int main()
{
	thread(print123).detach();
	thread(print123).detach();
	system("pause");

}
  1. 二叉樹前序遍歷
class Solution { // 非遞歸
public:
    vector<int> preorderTraversal(TreeNode* root) {
        vector<int> res;
        if (root == nullptr) {
            return res;
        }

        stack<TreeNode*> stk;
        TreeNode* node = root;
        while (!stk.empty() || node != nullptr) {
            while (node != nullptr) {
                res.emplace_back(node->val);
                stk.emplace(node);
                node = node->left;
            }
            node = stk.top();
            stk.pop();
            node = node->right;
        }
        return res;
    }
};

class Solution { // 遞歸
public:
    void dfs(TreeNode* root, vector<int> &vec){
        if(!root) return;
        vec.push_back(root->val);
        if(root->left) dfs(root->left, vec);
        if(root->right) dfs(root->right, vec);
    }
    vector<int> preorderTraversal(TreeNode* root) {
        vector<int> vec;
        dfs(root, vec);
        return vec;
    }
};
  1. 旋轉數組求最小
  2. 找出數字字符串中最長的連續上升子序列(連續上升:前后兩數之差為1),LC 300變種
  3. 之字形打印二叉樹
class Solution {
public:
    vector<vector<int>> levelOrder(TreeNode* root) {
        vector<vector<int>> vvi;
        if(!root) return vvi;
        queue<TreeNode*> q1, q2;
        vector<int> vi;
        bool flag = true;
        TreeNode* tmp;
        q1.push(root);
        while(q1.size()){
            while(q1.size()){
                tmp = q1.front();
                vi.push_back(tmp->val);
                if(tmp->left) q2.push(tmp->left);
                if(tmp->right) q2.push(tmp->right);
                q1.pop();
            }
            if(!flag) reverse(vi.begin(), vi.end());
            vvi.push_back(vi);
            vi.clear();
            flag = !flag;
            swap(q1, q2);
        }
        return vvi;
    }
};
  1. 二維數組順時針旋轉90度
class Solution {
public:
    void rotate(vector<vector<int>>& matrix) {
        for(int i = 0;i < matrix.size()/2;++i){
            for(int j = 0;j < matrix[0].size();++j){
                swap(matrix[i][j], matrix[matrix.size()-1-i][j]);
            }
        }
        for(int i = 0;i < matrix.size();++i){
            for(int j = i+1;j < matrix[0].size();++j){
                swap(matrix[i][j], matrix[j][i]);
            }
        }
    }
};
  1. k個一組反轉鏈表(倒置鏈表)
class Solution {
public:
    // 翻轉一個子鏈表,並且返回新的頭與尾
    pair<ListNode*, ListNode*> myReverse(ListNode* head, ListNode* tail) {
        ListNode* prev = tail->next;
        ListNode* p = head;
        while (prev != tail) {
            ListNode* nex = p->next;
            p->next = prev;
            prev = p;
            p = nex;
        }
        return {tail, head};
    }

    ListNode* reverseKGroup(ListNode* head, int k) {
        ListNode* hair = new ListNode(0);
        hair->next = head;
        ListNode* pre = hair;

        while (head) {
            ListNode* tail = pre;
            // 查看剩余部分長度是否大於等於 k
            for (int i = 0; i < k; ++i) {
                tail = tail->next;
                if (!tail) {
                    return hair->next;
                }
            }
            ListNode* nex = tail->next;
            pair<ListNode*, ListNode*> result = myReverse(head, tail);
            head = result.first;
            tail = result.second;
            // 把子鏈表重新接回原鏈表
            pre->next = head;
            tail->next = nex;
            pre = tail;
            head = tail->next;
        }

        return hair->next;
    }
};
class Solution {
public:
    ListNode* reverseKGroup(ListNode* head, int k) {
        if (k <= 1) return head;
        vector<ListNode* > vec;
        while (head) {
            vec.emplace_back(head);
            head = head->next;
        }
        if (vec.size() < k) return head;
        ListNode* newHead = vec[k - 1];
        for (int i = 1; i < k; ++i) {
            vec[i]->next = vec[i - 1];
        }
        int last_index = 0;
        if (vec.size() == k) {
            vec[0]->next = nullptr;
            return vec[k - 1];
        }
        int j = k, tmp;

        while (j < vec.size()) {
            tmp = last_index + k - 1;
            if (tmp + 1 < vec.size()) vec[tmp + 1]->next = nullptr;
            if (tmp + k >= vec.size()) {
                if(tmp+2 < vec.size()) vec[tmp+1]->next = vec[tmp+2];
                vec[last_index]->next = vec[tmp + 1];
                return newHead;
            }
            j = tmp + 2;

            for (; j < vec.size() && j - tmp <= k; ++j) {
                vec[j]->next = vec[j - 1];
            }
            vec[last_index]->next = vec[j - 1];
            last_index = tmp + 1;
        }

        return newHead;
    }
};
  1. 歸並鏈表(鏈表排序)
// 優先隊列
struct cmp{
    bool operator()(ListNode* a, ListNode* b){
        return a->val > b->val;
    }
};

class Solution {
public:
    ListNode* sortList(ListNode* head) {
        if(!head || !head->next) return head;
        priority_queue<ListNode*, vector<ListNode*>, cmp> pq;
        while(head){
            pq.push(head);
            head = head->next;
        }
        head = pq.top();
        pq.pop();
        ListNode* tmp = head;
        while(pq.size()){
            tmp->next = pq.top();
            pq.pop();
            tmp = tmp->next;
        }
        tmp->next = nullptr;
        return head;
    }
};
class Solution { // 歸並
public:
    ListNode* sortList(ListNode* head) {
        if (!head || !head->next) return head;
        auto slow = head, fast = head;
        while (fast->next && fast->next->next)
            slow = slow->next, fast = fast->next->next;
        fast = slow->next, slow->next = nullptr;
        return merge(sortList(head), sortList(fast));
    }

private:
    ListNode* merge(ListNode* l1, ListNode* l2) {
        ListNode* head = new ListNode(0);
        ListNode* ptr = head;
        while (l1 && l2) {
            ListNode* &node = l1->val < l2->val ? l1 : l2;
            ptr = ptr->next = node, node = node->next;
        }
        ptr->next = l1 ? l1 : l2;
        return head->next;
    }
};
  1. 下一個排列(下一個更大的數)
class Solution {
public:
    void nextPermutation(vector<int>& nums) {
        if(nums.size() <= 1) return;
        int i = nums.size()-1;
        for(;i > 0 && nums[i] <= nums[i-1];--i);
        if(i == 0){
            sort(nums.begin(), nums.end());
            return;
        }
        int index = i;
        for(int j = i+1;j < nums.size();++j){
            if(nums[j] < nums[index] && nums[j] > nums[i-1]){
                index = j;
            }
        }
        swap(nums[i-1], nums[index]);
        sort(nums.begin()+i, nums.end());
    }
};
  1. 鏡像二叉樹
class Solution {
public:
    void exchange(TreeNode* root){
        if(!root) return;
        TreeNode* tmp;
        tmp = root->right;
        root->right = root->left;
        root->left = tmp;
        if(root->left) exchange(root->left);
        if(root->right) exchange(root->right);
    }

    TreeNode* mirrorTree(TreeNode* root) {
        exchange(root);
        return root;
    }
};
  1. 多線程順序打印0到100
#include <iostream>
#include <algorithm>
#include <vector>
#include <unordered_map>
#include <unordered_set>
#include <map>
#include<queue>
#include <set>
#include<thread>
#include <stdio.h>
#include<mutex>
#include<chrono>

using namespace std;

int gdata = 0;
mutex mt;

void print(string s){
    while(gdata < 100){
        mt.lock();
        if(gdata >= 100) return;
        cout << s << " " << gdata << endl;
        ++gdata;
        mt.unlock();
        this_thread::sleep_for(chrono::milliseconds(5));
    }
}

int main()
{
    thread t1(print, "t1"), t2(print, "t2"), t3(print, "t3");
    t1.detach();
    t2.detach();
    t3.detach();
    system("pause");
    return 0;
}
  1. 生產者消費者簡易模型
#include <iostream>
#include <algorithm>
#include <vector>
#include <unordered_map>
#include <unordered_set>
#include <map>
#include <random>
#include<queue>
#include <set>
#include<thread>
#include <time.h>
#include <stdio.h>
#include<mutex>
#include<chrono>

using namespace std;

queue<int> que;
mutex mt;

void create_data(string s){

    for(int i = 0;i < 100;++i){
        mt.lock();
        que.push(rand()%100);
        cout << s << " " << que.back() << endl;
        mt.unlock();
        this_thread::sleep_for(chrono::microseconds(1000));
    }
}

void eat_data(string s){
    for(int i = 0;i < 100;++i){
        mt.lock();
        if(que.empty()){
            mt.unlock();
            this_thread::sleep_for(chrono::microseconds(1000));
            --i;
            continue;
        }
        cout << s << " " << que.front() << endl;
        que.pop();
        mt.unlock();
    }
}


int main()
{
    srand((unsigned) time(NULL));

    thread create(create_data, "create1"), eat(eat_data, "eat1"), create2(create_data, "create2"), eat2(eat_data, "eat2");
    eat.detach();
    create.detach();
    create2.detach();
    eat2.detach();
    system("pause");
    return 0;
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM