下面的內容針對每種資料結構詳細介紹,針對每種資料結構都會列出常遇到的經典問題和實現方法,主要是從JS角度實現,不過只要思路明白,至於到底用什麼語言,在本文中並不是那麼重要了
資料結構
-
陣列
對於陣列,是吾裡程式設計人最熟悉的資料結構了,還記得學生時代經常拿陣列和連結串列比較
說的最多的就是兩點:查詢和插入刪除。當你需要高頻做插入刪除時,選擇連結串列;需要高頻查詢時,選擇陣列。
- 陣列插入刪除:插入刪除一項,首先需要找到滿足具體條件的位置,然後當你插入刪除一項時需要移動其他所有項,這樣才能空出或填滿插入刪除的位置,這樣就動員了所有項,高頻的做這些操作,效能上遠沒有連結串列直接修改next。
- 連結串列查詢:若想要查詢某個具體位置的節點,需要從頭節點依次遍歷;而陣列有下標,可以直接訪問該下標的某項。
常見的陣列問題
- 查詢陣列中第k小的元素
利用快排思想,left[] (<) right[],從小到大排序,若left裡面個數m<k,則需要在right[]中找第k-m項,否則在left[]中找第k項
實現:
//查詢陣列中第K小的元素
var kArr = function(arr,k){
if(arr.length < k){console.log("沒有這麼多數呀!");return}
//結合快速排序
var pivotIndex = Math.floor(arr.length / 2);
var pivot = arr.splice(pivotIndex, 1)[0];
var left = [];
var right = [];
for (var i = 0; i < arr.length; i++){
if (arr[i] < pivot) {
left.push(arr[i]);
} else {
right.push(arr[i]);
}
}
if(left.length<k){
return right[k-left.length];
}
else{
return left[k];
}
}
var arr = [1,3,2,5,11,43,22,77,45,12];
console.log(kArr(arr,6));
//這裡為什麼會選擇快速排序思想結合實現,因為相比其他的簡單排序而言,快速排序效率更高。
//具體的快速排序實現思想可見下面的排序分析內容
複製程式碼
- 查詢第一個沒有重複的陣列元素
實現:(1)直接用兩層for迴圈(2)掃一遍陣列,用map統計每個元素出現的次數val,再返回第一個val為1的項。
說到上面的第二個方法,筆者內心咯噔了一下,之前參加秋招,面試小姐姐問了一個簡單演算法問題,問我有什麼優化的地方,我當時想複雜了,完全沒往物件方向想,痛哭流涕,還是得多刷題才能找到感覺-.-
//查詢第一個沒有重複的元素
var oneArr = function(arr){
if(arr.length === 1){return arr[0]};
var obj = {};
arr.forEach(element => {
if(!obj[element]) obj[element] = 1;
else obj[element]++;
});
for(var key in obj){
if(obj[key] === 1) {
return key;
}
}
}
var arr = [1,3,2,5,11,3,22,77,45,1,77,0,0];
console.log(oneArr(arr));
複製程式碼
- 合併兩個排序好的陣列
實現:
對於JS實現很簡單,直接concat後sort就好了,如果不用這些方法,可以用兩個指標分別指向兩個陣列,讓兩個元素進行比較,把小的放到新陣列中,並使較小的元素的陣列指標加1,繼續比較,直到有一個陣列遍歷完,最後,把另一個陣列剩下的元素放到新陣列後即可。
- 重新排列陣列中的正數和負數
實現:
利用快速排序思想,將整數和負數分別放到right[]和left[]中,然後各自排序,最後concat
-
棧
棧的主要核心:先進後出;棧是一種特殊的線性表,僅能線上性表的一端操作,棧頂允許操作,棧底不允許操作。
遞迴函式
的實現就利用了棧這種資料結構,當一個遞迴函式被呼叫時,被調函式的區域性變數、形參的值以及一個返回地址就會儲存在遞迴工作棧中。執行時按照後進先出的順序,進行函式執行,完成遞迴操作。
-
使用棧計算字尾表示式
編譯原理中,我們利用棧的結構特性實現字尾表示式的計算。
例:中綴表示式a + b * c + ( d * e + f ) * g,轉化為字尾表示式之後是a b c * + d e * f + g * +
具體的轉換過程:
1)如果遇到運算元,直接將其輸出
2)如果遇到操作符,則將其放入棧中,遇到左括號也將其放入棧中
3)如果遇到一個右括號,則將棧元素彈出,將彈出的操作符輸出直到遇到左括號為止,左括號只彈出不輸出
4)遇到其他的操作符例如 + ,* , (從棧中彈出元素直到遇到發現更低優先順序的元素或者棧空為止。彈出這些元素之後,才能將遇到的操作符壓入到棧中,有一點要注意,只有遇到 ) 的情況下才彈出 ( 其他情況下都不會彈出)
5)如果讀到了輸入的末尾,則將棧中的所有元素依次彈出
-
使用棧為棧中的元素排序
實現:先通過js-class實現棧結構,然後借用輔助棧help實現棧stack的排序
class Vect{
constructor(){
this.stack = [];
}
//入棧
in(num){
if (typeof num != "number") return false;
this.stack.push(num);
}
//出棧
out(){
if(this.stack.length>0){
let last = this.stack.pop();
return last;
}
}
//輸出
print(){
if(this.stack.length === 0){
console.log("棧空了");
}
else{
console.log(...this.stack);
}
}
}
//利用輔助棧對儲存棧排序
var sort = function(stack){
var help = new Vect();
while(stack.stack.length){
var pop = stack.out();
if(help.stack.length&&help.stack[help.stack.length-1]<pop) {//裡面的判斷順序不能顛倒,否則出現 java.util.EmptyStackException
stack.in(help.out());//當滿足help不為空,且help的元素小於pop(這樣排出的順序頂到底是從小到大的)
} //將help裡的元素返回到stack中
help.in(pop);//無論什麼情況,只要stack不為空,都將pop壓入help
}
while(help.stack.length) {//當help不為空的時候,help裡面的元素頂到底是從小到大的,
stack.in(help.out());//所以將help彈到stack中是頂到底是從大到小的
}
stack.print();
}
var stack = new Vect();
stack.in(2);
stack.in(1);
stack.in(5);
sort(stack);
複製程式碼
詳細排序過程:
- in入棧stack[2,1,5],建立空棧help
- stack彈出pop=5,由於此時help為空,直接in入help棧
- 接著,stack彈出pop=1,由於1<5,直接in入help棧(保證help棧是底-頂:大-小)
- 再,stack彈出pop=2,由於1<2,將help中pop=1直接in入棧stack,然後將2入棧help中
- 繼續,stack彈出pop=1,由於2>1,直接in入help棧
- 最後將help全都彈出放到stack中即有序的棧生成。
help棧
stack棧-
檢查字串中括號是否匹配正確
這裡為了簡化,直接就判斷()是否匹配,若有需要其他符號,可以增加判斷
實現思路:
實現過程中,預設先"("後")",若最開始遇到的事")",則直接跳出,顯示"右括號多了"。
- 掃描str
- 遇到"(",入棧stack
- 遇到")",判斷stack,若為空,右括號多了,返回false;若不為空,判斷top棧頂,若不為左括號,則不匹配,返回false;若為左括號,出棧,繼續下一個
- 掃描結束後,判斷stack棧中是否為空,若不為空則說明還有左括號沒有匹配完,左括號多了,返回false;否則匹配成功,返回true
//判斷()是否匹配
var match = function(str){
var strStack = new Vect();
//掃描str
var strArr = str.split('');
var p = 0;
while(p<strArr.length){
if(strArr[p]==="("){
strStack.in(strArr[p]);
}
if(strArr[p]===")"){
if(strStack.stack.length===0){
console.log("右括號多了");
return false;
}
else if(strStack.stack[strStack.stack.length-1]!=="("){
console.log("不匹配");
return false;;
}
else{
strStack.out();
}
}
p++;
}
//結束後,如果棧中還有,表示有左括號沒匹配完
if(strStack.stack.length){
console.log("左括號多於右括號");
return false;
}
else{
console.log("左右括號匹配正確");
return true;
}
}
match("((a+b)*v)/2)");
複製程式碼
-
佇列
佇列剛好和棧相反,核心即先進先出,實現方法和棧類似,區別上就是入隊和出隊的順序問題,不贅述。
-
連結串列
連結串列就是通過node和next連起來的一條鏈,本文中就簡單介紹單連結串列的結構實現和相關問題的實現
- JS實現單連結串列結構,實現連結串列新增、刪除、查詢、反轉
//結點
class Node{
constructor(element){
this.element = element;
this.next = null;
}
}
//連結串列
class LinkedList{
constructor(){ //建構函式
this.length = 0;
this.head = null;
}
append(element){ //追加結點
let node = new Node(element);
let current;
if(this.head == null) this.head = node;
else{
current = this.head;
while(current.next){
current = current.next;
}
current.next = node;
}
this.length++;
}
removeAt(position){ //刪除指定位置的結點
if(position >-1 && position < this.length){
let current = this.head;
let index = 0;
let previous;
if(position == 0){
this.head = current.next;
}else{
while(index++ < position){
previous = current;
current= current.next;
}
previous.next = current.next;
}
this.length--;
return current.element;
}
else{
return null;
}
}
insert(position,element){ //插入
if(position >-1 && position <= this.length){
let node = new Node(element);
let current = this.head;
let index = 0;
let previous;
if(position==0){
node.next = current;
this.head = node;
}else{
while(index++<position){
previous = current;
current = current.next;
}
previous.next = node;
node.next = current;
}
this.length++;
return true;
}else{
return false;
}
}
toString(){ //轉成字串
let current = this.head;
let str = '';
while(current){
str += ','+current.element;
current = current.next;
}
return str;
}
indexOf(element){ //索引
let current = this.head;
let index = 0;
while(current){
if(current.element == element){
return index;
}
index++;
current = current.next;
}
return -1;
}
reverse(){ //反轉
if(this.head === null || this.head.next===null) return;
let current = this.head;
let pnext = current.next;
current.next = null;
while(pnext){
let pp = pnext.next;
pnext.next = current;
current = pnext;
pnext = pp;
}
this.head = current;
}
}
let link = new LinkedList();
link.append("111");
link.append("222");
link.append("333");
link.reverse();
console.log(link);
console.log(link.indexOf("111"));
複製程式碼
反轉結果:
- 檢查連結串列中是否有迴圈
- 返回連結串列倒數第n個元素
- 移除連結串列中的重複元素
-
圖
解釋:圖(Graph)是由頂點的有窮非空集合和頂點之間邊的集合組成,通常表示為:G(V,E),其中,G表示一個圖,V是圖G中頂點的集合,E是圖G中邊的集合。在圖中的資料元素,我們稱之為頂點(Vertex),頂點集合有窮非空。在圖中,任意兩個頂點之間都可能有關係,頂點之間的邏輯關係用邊來表示,邊集可以是空的。
-
判斷圖是否為樹:確保圖是連通的,不含環的圖
(1)是否有環:要兩個陣列,一個二維陣列作為圖的鄰接矩陣,一個一維陣列標記某個節點是否遍歷過 (2)是否連通:檢查上面的一維陣列是否有遍歷到
-
統計圖中邊的個數,n節點:完全有向圖
n(n-1)
,完全無向圖n(n-1)/2
-
樹
N叉樹、平衡樹、二叉樹、二叉查詢樹、平衡二叉樹、紅黑樹、B樹
二叉樹(節點分支<=2)
總結:
- 有
n
個節點的二叉樹,分支樹為n-1
- 若二叉樹的高度為
h
,則最少有h
個節點,最多2^h -1
個節點(滿二叉樹) - 含有
n
個節點的二叉樹,高度最大n,高度最小log2(n+1)
向上取整 - 具有
n
個節點的完全二叉樹,高度為log2(n+1)
向上取整 - 哈夫曼樹:權值最小的二叉樹
平衡二叉樹
非葉子節點最多兩個子節點;左子節點小於右子節點;左右兩邊層級相差不大於1;沒有相同重複節點
紅黑樹也是一種平衡二叉樹
-
雜湊表
排序演算法
排序演算法,這裡主要詳細介紹四種,描述筆者切身理解,日後會繼續疊加其他內容
演算法複雜度
怎麼定義一種排序演算法穩定不穩定?
(1)穩定:排序前a在b前,a=b,排序後a仍在b前(冒泡
、插入
、歸併
、基數
)
(2)不穩定:排序前a在b前,a=b,排序後a可能在b後(選擇
、快速
、希爾
、堆
)
下面從小到大
排序依次分析各種實現:
-
氣泡排序
從陣列中第一個數開始,依次遍歷陣列中的每一個數,通過相鄰比較交換,每一輪迴圈下來找出剩餘未排序數的中的最大數並”冒泡”至陣列的最後一個。
//冒泡
for(i=0;i<len-1;i++){
for(j=0;j<len-1-i;j++){//每一輪最後一個元素都是最值,所以可以不用再比
if(arr[j]>arr[j+1]){
var temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
複製程式碼
-
選擇排序
從所有記錄中選出最小的一個資料元素與第一個位置的記錄交換;然後在剩下的記錄當中再找最小的與第二個位置的記錄交換,迴圈到只剩下最後一個資料元素為止。
//選擇
for(i=0;i<len-1;i++){
var minIndex = i;
for(j=i+1;j<len;j++){
if(arr[j]<arr[minIndex]){
minIndex = j;
}
}
var temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
複製程式碼
-
插入排序
從待排序的n個記錄中的第二個記錄開始,依次與前面的記錄比較並尋找插入的位置,每次外迴圈結束後,將當前的數插入到合適的位置。
//插入
for(i=1;i<len;i++){
if(arr[i]<arr[i-1]){
var temp = arr[i];
var j = i-1;
while(j>=0 && temp<arr[j]){
arr[j+1] = arr[j];
j--;
}
arr[j+1] = temp;
}
}
複製程式碼
插入排序優化:即找到要插入的位置時,我們可以用二分查詢來找到該位置
// 優化(二分查詢)
for(var i = 1;i<len;i++){
var key = arr[i];
var j = i-1;
var right = i-1;
var left = 0;
while(left<=right){
var mid = parseInt((left+right)/2);
if(key<arr[mid]){
right = mid-1;
}
else{
left = mid+1;
}
}
// 這裡最終找到的是left
for(var j=i+1;j>=left;j--){
arr[j+1] = arr[j];
}
arr[left] = key;
}
複製程式碼
-
快速排序
從數列中挑出一個元素為基準,另外建立兩個陣列left和right,把比基準小的放在left中,把比基準大的放在right中,並且依此遞迴,最終並接兩個陣列得到的就是排序後的陣列。
var quickSort2 = function(arr) {
if (arr.length <= 1) { return arr; }
var pivotIndex = Math.floor(arr.length / 2);
var pivot = arr.splice(pivotIndex, 1)[0];
var left = [];
var right = [];
for (var i = 0; i < arr.length; i++){
if (arr[i] < pivot) {
left.push(arr[i]);
} else {
right.push(arr[i]);
}
}
return quickSort2(left).concat([pivot], quickSort2(right));
};
複製程式碼
-
希爾排序
-
歸併排序
-
堆排序
-
基數排序
裡面有些內容還沒有補充,持續整理更新。。。有錯誤請指教,共同進步
複製程式碼