JavaScript的數據結構與算法

對數據結構進行記錄,便於查看。

算法

1. 算法複雜度

1.1 O表示的複雜度

O的幾種經常使用表達
O的幾種經常使用表達

1.2 O(1)

賦值就是O(1)複雜度的,常數級的複雜度。chrome

1.3 O(logn)

對數級別的算法複雜度,特色是數組數量越大時,增加的越緩慢。 在大量數據時性能較好數組

典型的是二分查找法:瀏覽器

function binarySearch(arr, target){
    let start = 0;
    let end = arr.length - 1;
    
    while(start < end){
        let mid = parseInt((start + end) / 2)
        if(target < arr[mid]){
            end = mid - 1;
        }else if(target > arr[mid]){
            start = mid + 1;
        }else{
            return mid;
        }
    }
    return -1;
}
複製代碼

1.4 O(n)

典型的有順序搜索算法。bash

function search(arr, targeet){
    let index = -1;
    for(let i = 0; i < arr.legth; i++){
        if(arr[i] === target){
            index = i;
        }
    }
    return index;
}
複製代碼

1.5 O(n2)

平方級別的複雜度要儘可能避免。 典型的有冒泡排序。數據結構

function bubbleSort(arr){
    let len = arr.length;
    for(let i = 0; i < len-1; i++){  //對前len-1個數都進行一邊排序
        for(let j = 0; j < len - 1 - i;  j++){
            if(arr[j] > arr[j+1]){
                let temp = arr[j];
                arr[j] = arr[j+1];
                arr[j+1] = temp;
            }
        }
    }
    return arr;
}
複製代碼

2. 排序算法

2.1 快速排序

chrome中sort()函數默認用的是快速排序。
三步走:ide

  • 從數組中取出一個數做爲基數
  • 分區,將數組分爲兩部分,比基數小的放在數組的左邊,比基數大的放到數組的右邊。
  • 遞歸使左右區間重複第二步,直至各個區間只有一個數。

快速排序是分治策略的經典實現,分治的策略以下:函數

  • 分解(Divide)步驟:將問題劃分未一些子問題,子問題的形式與原問題同樣,只是規模更小
  • 解決(Conquer)步驟:遞歸地求解出子問題。若是子問題的規模足夠小,則中止遞歸,直接求解
  • 合併(Combine)步驟:將子問題的解組合成原問題的解

快速排序函數,咱們須要將排序問題劃分爲一些子問題進行排序,而後經過遞歸求解,咱們的終止條件就是,當array.length > 1再也不生效時返回數組。性能

阮一峯版本的快排引發了很大的爭議,主要是兩方面:測試

  1. 取中間值不能用splice,直接用下標取值
  2. 用原地數組交換,不要每次循環都新建一個數組

時間複雜度O(nlogn),當數據有序時,以第一個關鍵字爲基準分爲兩個子序列,前一個子序列爲空,此時效率最低,因此數據越隨機,性能越好,數據越接近有序,性能越差。 空間複雜度O(nlogn), 每次挖坑(找基準數)過程當中須要一個空間存儲基數,快排大概須要nlogn次的處理,因此空間也是nlogn。

快排中的切分方式

快排中最重要的步驟就是將小於等於中軸元素的整數放到中軸元素的左邊,將大於中軸元素的數據放到中軸元素的右邊,這裏咱們把該步驟定義爲'切分'。以首元素做爲中軸元素,下面介紹幾種常見的'切分方式'。

1. 兩端掃描,一端挖坑,一端填補

使用兩個變量i和j,i指向最左邊的元素,j指向最右邊的元素,咱們將首元素做爲中軸,將首元素複製到變量pivot中,這時咱們能夠將首元素i所在的位置當作一個坑,咱們從j的位置從右向左掃描,找一個小於等於中軸的元素A[j],來填補A[i]這個坑,填補完成後,拿去填坑的元素所在的位置j又能夠看作一個坑,這時咱們在以i的位置從前日後找一個大於中軸的元素來填補A[j]這個新的坑,如此往復,直到i和j相遇(i==j,此時i和j指向同一個坑)。最後咱們將中軸元素放到這個坑中。最後對左半數組和右半數組重複上述操做。

2. 從兩端掃描交換

使用兩個變量i和j,i指向首元素的元素下一個元素(最左邊的首元素爲中軸元素),j指向最後一個元素,咱們從前日後找,直到找到一個比中軸元素大的,而後從後往前找,直到找到一個比中軸元素小的,而後交換這兩個元素,直到這兩個變量交錯(i > j)(注意不是相遇 i == j,由於相遇的元素還未和中軸元素比較)。最後對左半數組和右半數組重複上述操做。

function quick(arr,left,right){
    if(left < right){//遞歸的邊界條件,當 left === right時數組的元素個數爲1個
        let pivot = arr[left];//最左邊的元素做爲中軸
        let i = left+1, j = right;
        //當i == j時,i和j同時指向的元素尚未與中軸元素判斷,
        //小於等於中軸元素,i++,大於中軸元素j--,
        //當循環結束時,必定有i = j+1, 且i指向的元素大於中軸,j指向的元素小於等於中軸
        while(i <= j){
            while(i <= j && arr[i] <= pivot){
                i++;
            }
            while(i <= j && arr[j] > pivot){
                j--;
            }
            //當 i > j 時整個切分過程就應該中止了,不能進行交換操做
            //這個能夠改爲 i < j, 這裏 i 永遠不會等於j, 由於有上述兩個循環的做用
            if(i <= j){
                swap(arr, i, j);
                i++;
                j--;
            }
        }
        //當循環結束時,j指向的元素是最後一個(從左邊算起)小於等於中軸的元素
        swap(arr, left, j);//將中軸元素和j所指的元素互換
        quick(arr, left, j-1);//遞歸左半部分
        quick(arr, j+1, right);//遞歸右半部分
    }
    function quickSort(arr){
        quick(arr,0,arr.length-1)
    }
}
複製代碼
3. 從一端掃描

仍是選最左邊的數據爲基準,arr[1,i]表示小於等於pivot的部分,i指向中軸元素,表示小於等於pivot的元素個數爲0,j之後的都是未知元素,即不知道比pivot大,仍是比中軸元素小。j初始化指向第一個未知元素。

arr[j]大於pivot時,j前進;
arr[j]小於等於pivot時,注意i的位置,i的後一個元素就是大於pivot的元素,交換a[i+1]和a[j],交換後小於等於pivot的部分增長1,j增長1,繼續掃描下一個。而i的下一個元素仍然大於pivot,又回到了先前的狀態。

function quick(arr,left, right){
    if(lift < right){
        int pivot = arr[left];//最左邊的元素做爲中軸元素
        //初始化時小於等於pivot的部分,元素個數爲0
        //大於pivot的部分,元素個數也爲0
        int i = left, j = left+1;
        while(j <= right){
            if(arr[j] <= pivot){
                i++;
                swap(arr, i, j);
                j++;//j繼續向前,掃描下一個
            }else{
                j++;//大於pivot的元素增長一個
            }
        }
        //arr[i]及arr[i]之前的都小於等於pivot
        //循環結束後arr[i+1]及它之後的都大於pivot
        //因此交換arr[left]和arr[i],這樣咱們就將中軸元素放到了適當的位置
        swap(arr, left, i);
        quick(arr, left, i-1);
        quick(arr, i+1, right);
    }
}
function quickSort(arr){
    quick(arr,0,arr.length-1)
}
複製代碼

快排中的優化

三向切分的快速排序
雙軸快速排序

2.2 歸併排序

歸併排序的實現

firefox瀏覽器的sort()函數默認方法。 將兩個已經排序的數組合並,要比將無序的數組合並快。 歸併排序和快速排序均可以用構成二叉樹來解釋,只不過快排的複雜度花在了成樹上(二叉搜索樹,從上往下),歸併的複雜度花在了歸併上,

function mergeSort(arr){
    let len = arr.length;
    if(len<=1){
        return arr;
    }
    let mid = Math.floor(len/2),
        leftArr = arr.slice(0,mid),
        rightArr = arr.slice(mid);
    return merge(mergeSort(leftArr),mergeSort(rightArr))
}
//merge負責合併
function merge(leftArr, rightArr){
    let result = [];
    while(leftArr.length && rightArr.length){
        if(leftArr[0]<=rightArr[0]){
            result.push(leftArr.shift());
        }else{
            result.push(rightArr.shift());
        }
    }
    while(leftArr.length){
        result.push(leftArr.shift());
    }
    while(rightArr.length){
        result.push(rightArr.shift());
    }
    return result;
}
複製代碼

棧溢出問題及解決

mergeSort會致使很頻繁的自調用,一個長度爲n的數組最終會調用2*(n-1)次mergeSort()。
若是須要排序的數組很長可能會在某些棧小的瀏覽器上發生溢出錯誤。
棧大小能夠測試:

var cnt = 0;
try {
  (function() {
    cnt++;
    arguments.callee();
  })();
} catch(e) {
  console.log(e.message, cnt);
}
複製代碼

遇到棧溢出錯誤並不必定要修改整個算法,只是代表遞歸不是最好的實現方式。這個合併排序算法一樣能夠迭代實現,好比(摘抄自《高性能JavaScript》):

function merge(left, right) {
  var result = [];

  while (left.length && right.length) {
    if (left[0] < right[0])
      result.push(left.shift());
    else
      result.push(right.shift());
  }

  return result.concat(left, right);
}

function mergeSort(a) {
  if (a.length === 1)
    return a;

  var work = [];
  for (var i = 0, len = a.length; i < len; i++)
    work.push([a[i]]);

  work.push([]); // 若是數組長度爲奇數

  for (var lim = len; lim > 1; lim = ~~((lim + 1) / 2)) {
    for (var j = 0, k = 0; k < lim; j++, k += 2) 
      work[j] = merge(work[k], work[k + 1]);

    work[j] = []; // 若是數組長度爲奇數
  }

  return work[0];
}

console.log(mergeSort([1, 3, 4, 2, 5, 0, 8, 10, 4]));
複製代碼

這個版本的mergeSort()函數功能與前例相同卻沒有使用遞歸。儘管迭代版本的合併排序算法比遞歸實現要慢一些,但它並不會像遞歸版本那樣受調用棧限制的影響。把遞歸算法改用迭代實現是實現棧溢出錯誤的方法之一。

2.3 冒泡排序和選擇排序

都是O(N2)的複雜度。 冒泡前文有, 選擇排序相似,也是對相鄰進行兩兩比較,不一樣的是否是每比較一次就換位置,而是一輪比較完畢找到最大或最小值後放到正確的位置。

function selectionSort(arr){
	let len=arr.length;
    let minIndex,temp;

	for(let i=0;i<len-1;i++){

		minIndex=i;

		for(var j=i+1;j<len;j++){

			if(arr[j]<arr[minIndex]){       //尋找最小的數

				minIndex=j;                //將最小數的索引保存

			}

		}

		temp=arr[i];

		arr[i]=arr[minIndex];

		arr[minIndex]=temp;

	}

	return arr;
複製代碼

3.其餘算法

3.1 求二進制數中1的個數

任意給定一個32位無符號整數n,求n的二進制表示中1的個數,好比n = 5(0101)時,返回2,n = 15(1111)時,返回4

3.1.1普通法

function bitCount(n){
    let count = 0;
    while(n>0){
        if((n&1)===1){
            count++;
        }
        n = n >> 1;
    }
    return count;
}
複製代碼
function bitCount(n){
    let count = 0;
    for(count = 0; n; n=n>>1){
        count += n&1;
    }
    return count;
}
複製代碼

3.1.2 快速法

速度較快,與n的大小無關,只與1的個數有關。 原理是n&(n-1)至關於清除最低位的1,從二進制的角度講,n至關於在n-1的最低位加上1。

function bitCount(n){
    let count = 0;
    for(count = 0; n; count++){
        n=n&(n-1)
    }
    return count;
}
複製代碼

3.1.3 製表法

3.1.3.1 動態建表

原理:根據奇偶性來分析,任一個正整數n,

  1. n是偶數,那麼n的二進制中1的個數與n/2中1的個數是相同的,好比4和2的二進制中都有一個1,6和3的二進制中都有兩個1。爲啥?由於n是由n/2左移一位而來,而移位並不會增長1的個數。
  2. 若是n是奇數,那麼n的二進制中1的個數是n/2中1的個數+1,好比7的二進制中有三個1,7/2 = 3的二進制中有兩個1。爲啥?由於當n是奇數時,n至關於n/2左移一位再加1。

再說一下查表的原理

對於任意一個32位無符號整數,將其分割爲4部分,每部分8bit,對於這四個部分分別求出1的個數,再累加起來便可。而8bit對應2^8 = 256種01組合方式,這也是爲何表的大小爲256的緣由。

注意類型轉換的時候,先取到n的地址,而後轉換爲unsigned char*,這樣一個unsigned int(4 bytes)對應四個unsigned char(1bytes),分別取出來計算便可。舉個例子吧,以87654321(十六進制)爲例,先寫成二進制形式-8bit一組,共四組,以不一樣顏色區分,這四組中1的個數分別爲4,4,3,2,因此一共是13個1,以下面所示。

10000111 01100101 01000011 00100001 = 4 + 4 + 3 + 2 = 13
C++源碼:

int BitCount(unsigned int n) 
{ 
    // 建表
    unsigned char BitsSetTable256[256] = {0} ; 

    // 初始化表 
    for (int i =0; i <256; i++) 
    { 
        BitsSetTable256[i] = (i &1) + BitsSetTable256[i /2]; 
    } 

    unsigned int c =0 ; 
    
    // 查表
    unsigned char* p = (unsigned char*) &n ; 

    c = BitsSetTable256[p[0]] + 
        BitsSetTable256[p[1]] + 
        BitsSetTable256[p[2]] + 
        BitsSetTable256[p[3]]; 

    return c ; 
}
複製代碼

js:

function bitCount(n){
    //建表
    let bitsSetTable256 = [0];
    //初始化表
    for(let i=0;i<256;i++){
        bitsSetTable256[i] = (i & 1) + bitsSetTable256[Math.floor(i/2)];
    }
    let count = 0;
    count = bitsSetTable256[n & 0xff] + bitsSetTable256[(n>>8) & 0xff] + bitsSetTable256[(n>>8) & 0xff] + bitsSetTable256[(n>>8) & 0xff];
    return count;
}
複製代碼
3.1.3.2 靜態建表4bit
int BitCount(unsigned int n) {
    unsigned int table[16] = 
    {
        0, 1, 1, 2, 
        1, 2, 2, 3, 
        1, 2, 2, 3, 
        2, 3, 3, 4
    } ;

    unsigned int count =0 ;
    while (n)
    {
        count += table[n &0xf] ;
        n >>=4 ;
    }
    return count ;
}
複製代碼
3.1.3.3 靜態建表8bit
int BitCount(unsigned int n) { 
    unsigned int table[256] = 
    { 
        0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4, 
        1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 
        1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 
        2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 
        1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 
        2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 
        2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 
        3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 
        1, 2, 2, 3, 2, 3, 3, 4, 2, 3, 3, 4, 3, 4, 4, 5, 
        2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 
        2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 
        3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 
        2, 3, 3, 4, 3, 4, 4, 5, 3, 4, 4, 5, 4, 5, 5, 6, 
        3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 
        3, 4, 4, 5, 4, 5, 5, 6, 4, 5, 5, 6, 5, 6, 6, 7, 
        4, 5, 5, 6, 5, 6, 6, 7, 5, 6, 6, 7, 6, 7, 7, 8, 
    }; 

    return table[n &0xff] +
        table[(n >>8) &0xff] +
        table[(n >>16) &0xff] +
        table[(n >>24) &0xff] ;
}
複製代碼

首先構造一個包含256個元素的表table,table[i]即i中1的個數,這裏的i是[0-255]之間任意一個值。而後對於任意一個32bit無符號整數n,咱們將其拆分紅四個8bit,而後分別求出每一個8bit中1的個數,再累加求和便可,這裏用移位的方法,每次右移8位,並與0xff相與,取得最低位的8bit,累加後繼續移位,如此往復,直到n爲0。因此對於任意一個32位整數,須要查表4次。

第一次(n & 0xff) 10101011110011011110111100010010

第二次((n >> 8) & 0xff) 00000000101010111100110111101111

第三次((n >> 16) & 0xff)00000000000000001010101111001101

第四次((n >> 24) & 0xff)00000000000000000000000010101011

固然也能夠建一個16bit的表,或者32bit,速度會更快。

3.1.4 平行算法

不必定是最快的, 將n寫成二進制,而後相鄰位增長,重複這個過程,直到只剩下一位。

function bitCount(n){
    n = (n &0x55555555) + ((n >>1) &0x55555555) ; 
    n = (n &0x33333333) + ((n >>2) &0x33333333) ; 
    n = (n &0x0f0f0f0f) + ((n >>4) &0x0f0f0f0f) ; 
    n = (n &0x00ff00ff) + ((n >>8) &0x00ff00ff) ; 
    n = (n &0x0000ffff) + ((n >>16) &0x0000ffff) ; 
    return n ; 
}
複製代碼
3.1.5 完美法
int BitCount(unsigned int n) 
{
    unsigned int tmp = n - ((n >>1) &033333333333) - ((n >>2) &011111111111);
    return ((tmp + (tmp >>3)) &030707070707) %63;
}
複製代碼

第一行代碼的做用

先說明一點,以0開頭的是8進制數,以0x開頭的是十六進制數,上面代碼中使用了三個8進制數。

將n的二進制表示寫出來,而後每3bit分紅一組,求出每一組中1的個數,再表示成二進制的形式。好比n = 50,其二進制表示爲110010,分組後是110和010,這兩組中1的個數本別是2和3。2對應010,3對應011,因此第一行代碼結束後,tmp = 010011,具體是怎麼實現的呢?因爲每組3bit,因此這3bit對應的十進制數都能表示爲2^2 * a + 2^1 * b + c的形式,也就是4a + 2b + c的形式,這裏a,b,c的值爲0或1,若是爲0表示對應的二進制位上是0,若是爲1表示對應的二進制位上是1,因此a + b + c的值也就是4a + 2b + c的二進制數中1的個數了。舉個例子,十進制數6(0110)= 4 * 1 + 2 * 1 + 0,這裏a = 1, b = 1, c = 0, a + b + c = 2,因此6的二進制表示中有兩個1。如今的問題是,如何獲得a + b + c呢?注意位運算中,右移一位至關於除2,就利用這個性質!

4a + 2b + c 右移一位等於2a + b

4a + 2b + c 右移量位等於a

而後作減法

4a + 2b + c –(2a + b) – a = a + b + c,這就是第一行代碼所做的事,明白了吧。

第二行代碼的做用

在第一行的基礎上,將tmp中相鄰的兩組中1的個數累加,因爲累加到過程當中有些組被重複加了一次,因此要捨棄這些多加的部分,這就是&030707070707的做用,又因爲最終結果可能大於63,因此要取模。

須要注意的是,通過第一行代碼後,從右側起,每相鄰的3bit只有四種可能,即000, 001, 010, 011,爲啥呢?由於每3bit中1的個數最多爲3。因此下面的加法中不存在進位的問題,由於3 + 3 = 6,不足8,不會產生進位。

tmp + (tmp >> 3)-這句就是是相鄰組相加,注意會產生重複相加的部分,好比tmp = 659 = 001 010 010 011時,tmp >> 3 = 000 001 010 010,相加得

001 010 010 011

000 001 010 010


001 011 100 101

011 + 101 = 3 + 5 = 8。(感謝網友Di哈指正。)注意,659只是箇中間變量,這個結果不表明659這個數的二進制形式中有8個1。

注意咱們想要的只是第二組和最後一組(綠色部分),而第一組和第三組(紅色部分)屬於重複相加的部分,要消除掉,這就是&030707070707所完成的任務(每隔三位刪除三位),最後爲何還要%63呢?由於上面至關於每次計算相連的6bit中1的個數,最可能是111111 = 77(八進制)= 63(十進制),因此最後要對63取模。

相關文章
相關標籤/搜索