前端媛需要知道的資料結構和簡單排序演算法---不能再少了

錢錢97發表於2018-12-07

下面的內容針對每種資料結構詳細介紹,針對每種資料結構都會列出常遇到的經典問題和實現方法,主要是從JS角度實現,不過只要思路明白,至於到底用什麼語言,在本文中並不是那麼重要了

資料結構

  • 陣列

對於陣列,是吾裡程式設計人最熟悉的資料結構了,還記得學生時代經常拿陣列和連結串列比較

說的最多的就是兩點:查詢和插入刪除。當你需要高頻做插入刪除時,選擇連結串列;需要高頻查詢時,選擇陣列。

  1. 陣列插入刪除:插入刪除一項,首先需要找到滿足具體條件的位置,然後當你插入刪除一項時需要移動其他所有項,這樣才能空出或填滿插入刪除的位置,這樣就動員了所有項,高頻的做這些操作,效能上遠沒有連結串列直接修改next。
  2. 連結串列查詢:若想要查詢某個具體位置的節點,需要從頭節點依次遍歷;而陣列有下標,可以直接訪問該下標的某項。

常見的陣列問題

  1. 查詢陣列中第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. 查詢第一個沒有重複的陣列元素

實現:(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));

複製程式碼
  1. 合併兩個排序好的陣列

實現:

對於JS實現很簡單,直接concat後sort就好了,如果不用這些方法,可以用兩個指標分別指向兩個陣列,讓兩個元素進行比較,把小的放到新陣列中,並使較小的元素的陣列指標加1,繼續比較,直到有一個陣列遍歷完,最後,把另一個陣列剩下的元素放到新陣列後即可。

  1. 重新排列陣列中的正數和負數

實現:

利用快速排序思想,將整數和負數分別放到right[]和left[]中,然後各自排序,最後concat

棧的主要核心:先進後出;棧是一種特殊的線性表,僅能線上性表的一端操作,棧頂允許操作,棧底不允許操作。

遞迴函式的實現就利用了棧這種資料結構,當一個遞迴函式被呼叫時,被調函式的區域性變數、形參的值以及一個返回地址就會儲存在遞迴工作棧中。執行時按照後進先出的順序,進行函式執行,完成遞迴操作。

  1. 使用棧計算字尾表示式

編譯原理中,我們利用棧的結構特性實現字尾表示式的計算。

例:中綴表示式a + b * c + ( d * e + f ) * g,轉化為字尾表示式之後是a b c * + d e * f + g * +

具體的轉換過程:

1)如果遇到運算元,直接將其輸出

2)如果遇到操作符,則將其放入棧中,遇到左括號也將其放入棧中

3)如果遇到一個右括號,則將棧元素彈出,將彈出的操作符輸出直到遇到左括號為止,左括號只彈出不輸出

4)遇到其他的操作符例如 + ,* , (從棧中彈出元素直到遇到發現更低優先順序的元素或者棧空為止。彈出這些元素之後,才能將遇到的操作符壓入到棧中,有一點要注意,只有遇到 ) 的情況下才彈出 ( 其他情況下都不會彈出)

5)如果讀到了輸入的末尾,則將棧中的所有元素依次彈出

  1. 使用棧為棧中的元素排序

實現:先通過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);
複製程式碼

詳細排序過程:

  1. in入棧stack[2,1,5],建立空棧help
  2. stack彈出pop=5,由於此時help為空,直接in入help棧
  3. 接著,stack彈出pop=1,由於1<5,直接in入help棧(保證help棧是底-頂:大-小)
  4. 再,stack彈出pop=2,由於1<2,將help中pop=1直接in入棧stack,然後將2入棧help中
  5. 繼續,stack彈出pop=1,由於2>1,直接in入help棧
  6. 最後將help全都彈出放到stack中即有序的棧生成。

help棧

help
stack棧
tack

  1. 檢查字串中括號是否匹配正確

這裡為了簡化,直接就判斷()是否匹配,若有需要其他符號,可以增加判斷

實現思路:

實現過程中,預設先"("後")",若最開始遇到的事")",則直接跳出,顯示"右括號多了"。

  1. 掃描str
  2. 遇到"(",入棧stack
  3. 遇到")",判斷stack,若為空,右括號多了,返回false;若不為空,判斷top棧頂,若不為左括號,則不匹配,返回false;若為左括號,出棧,繼續下一個
  4. 掃描結束後,判斷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連起來的一條鏈,本文中就簡單介紹單連結串列的結構實現和相關問題的實現

  1. 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"));

複製程式碼

反轉結果:

前端媛需要知道的資料結構和簡單排序演算法---不能再少了

  1. 檢查連結串列中是否有迴圈
  2. 返回連結串列倒數第n個元素
  3. 移除連結串列中的重複元素

解釋:圖(Graph)是由頂點的有窮非空集合和頂點之間邊的集合組成,通常表示為:G(V,E),其中,G表示一個圖,V是圖G中頂點的集合,E是圖G中邊的集合。在圖中的資料元素,我們稱之為頂點(Vertex),頂點集合有窮非空。在圖中,任意兩個頂點之間都可能有關係,頂點之間的邏輯關係用邊來表示,邊集可以是空的。

  1. 判斷圖是否為樹:確保圖是連通的,不含環的圖

    (1)是否有環:要兩個陣列,一個二維陣列作為圖的鄰接矩陣,一個一維陣列標記某個節點是否遍歷過 (2)是否連通:檢查上面的一維陣列是否有遍歷到

  2. 統計圖中邊的個數,n節點:完全有向圖n(n-1),完全無向圖n(n-1)/2

N叉樹、平衡樹、二叉樹、二叉查詢樹、平衡二叉樹、紅黑樹、B樹

二叉樹(節點分支<=2)

總結:

  1. n個節點的二叉樹,分支樹為n-1
  2. 若二叉樹的高度為h,則最少有h個節點,最多2^h -1個節點(滿二叉樹)
  3. 含有n個節點的二叉樹,高度最大n,高度最小log2(n+1)向上取整
  4. 具有n個節點的完全二叉樹,高度為log2(n+1)向上取整
  5. 哈夫曼樹:權值最小的二叉樹

平衡二叉樹

非葉子節點最多兩個子節點;左子節點小於右子節點;左右兩邊層級相差不大於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));
};
複製程式碼
  • 希爾排序

  • 歸併排序

  • 堆排序

  • 基數排序

裡面有些內容還沒有補充,持續整理更新。。。有錯誤請指教,共同進步
複製程式碼

相關文章