JavaScript 實現(xiàn)排序算法可視化手記

原創(chuàng)文章,未經(jīng)許可,請勿轉載
?Soyaine

這兩天找了個沒網(wǎng)的地方,總算把之前遺留下的排序可視化實現(xiàn)了,現(xiàn)在初步完成了冒泡排序和選擇排序,寫這篇是為了整理一下從零到實現(xiàn)的過程,也分享自己的解決思路,歡迎多多交流。

(很牛的視頻鏈接列表。)

下面主要記錄下選擇排序的實現(xiàn)。

失敗的嘗試

還沒開始做時,覺得只要在排序時每一步高亮一個數(shù)據(jù)塊,就能實現(xiàn)了。于是能想到的最簡單的是,寫一個實現(xiàn)暫停的 sleep 函數(shù),在每次循環(huán)后暫停,操作 DOM。我經(jīng)歷過的失敗的嘗試有:

  1. 在循環(huán)中操作節(jié)點樣式,無法控制延遲時間
  2. 在循環(huán)中混入 setTimeOut
  3. 在循環(huán)中混入元素交換的操作

瀏覽器的渲染機制及定時器機制

瀏覽器渲染

瀏覽器是單線程的,UI 引擎和 JS 引擎互斥,也就是說,在循環(huán)運行的過程中, JS 引擎在執(zhí)行循環(huán)操作時,無法調(diào)用 UI 引擎。所以涉及到重新渲染頁面的操作不會立即執(zhí)行,而是會排到任務隊列中,等循環(huán)結束(JS 引擎空閑)時再執(zhí)行渲染。所以能看到的效果是,頁面假死,到最后突然變成排序結束的樣子。

這時候就需要用到定時器,具體用法參見文檔。

在將 setTimeOutfor/while 循環(huán)一起使用的過程中,我遇到了兩個問題:

  1. 時序混亂。它也會在循環(huán)結束后才統(tǒng)一渲染,有可能會出現(xiàn)交叉的情況。
  2. 參數(shù)無法正確的傳遞。具體的問題是 setTimeOut 內(nèi)函數(shù)真正執(zhí)行時,所讀取到的是循環(huán)結束后的值,而不是想象的每次循環(huán)傳遞一個參數(shù)(參見 stackOverflow 的問題)。

由此進化得到的解決辦法是,把排序和渲染分離,排序時專注于數(shù)據(jù)的循環(huán)比較,得出每一幀所涉及到的節(jié)點索引以及對應狀態(tài),存入隊列備后續(xù)渲染使用,渲染時根據(jù)上一步提供的單幀數(shù)據(jù),用定時器進行 DOM 操作實現(xiàn)一步一步的動畫呈現(xiàn)。

setTimeOut 的循環(huán)實現(xiàn)

之所以會考慮將每幀的情況存入數(shù)列之后,再統(tǒng)一渲染,這是在用 setTimeOut 實現(xiàn)循環(huán)的過程中發(fā)現(xiàn)的。

Nicholas 在《JavaScript高級程序設計》中優(yōu)化性能的思路是將需要長時間運算的循環(huán)分解為“片”來進行運算:

// http://www.nczonline.net/blog/2009/01/13/speed-up-your-javascript-part-1/
function chunk(array, process, context){
  var items = array.concat();   //clone the array
  setTimeout(function(){
    var item = items.shift();
    process.call(context, item);

    if (items.length > 0){
      setTimeout(arguments.callee, 100); 
    }
  }, 100);
}

可以注意到其中 setTimeout(arguments.callee, 100) 一句將會為新的數(shù)據(jù)設定一個新的 timer。借用這種方法,我們可以實現(xiàn)數(shù)組元素的遍歷,即存入需要渲染的數(shù)據(jù)之后,只需使用 setTimeOut 依次讀取每條數(shù)據(jù)進行渲染即可。

選擇排序的分解

拿選擇排序來舉例,為便于描述,下面我用外循環(huán)和內(nèi)循環(huán)來指代選擇排序嵌套的兩層循環(huán)。

var arr; //待排序數(shù)組
var min; //存最小值
for (var outer = 0; outer < arr.length-1; outer++){
  min = outer;
  for (var inner = outer+1; inner < arr.length; inner++){
    if (arr[min] > arr[inner]){
      min = inner;
    }
  }
  swap(arr, min, inner);
}

下面這張圖是我的解決思路,主要有兩部分,一是 Array(排序數(shù)據(jù))操作,二是 DOM 操作,包括樣式和高度的改變。

hand writer

DOM 單幀分解

下面來設想一下排序過程中所涉及到的每一幀是以什么樣的樣式出現(xiàn)。可以回看上面的幾個視頻,思考過程是這樣,把整個排序過程分解,每一步也就對應了我們后來的每一幀。

想象我需要實現(xiàn)的效果:

  1. arr[0] 高亮
  2. arr[1] 高亮
  3. if (arr[min] > arr[1])
    1. true : arr[1] 標記為最小值
    2. false : arr[1] 取消高亮
  4. arr[2] 高亮
  5. ……循環(huán)

上面的過程中,為了區(qū)分不同類型的數(shù),思考一下有哪些狀態(tài):

  1. select: 待交換,每輪外循環(huán)所選定的值
  2. on: 正在比較中,即每輪內(nèi)循環(huán)依次遍歷的值
  3. min: 內(nèi)循環(huán)的目的是找到此輪的最小值,所以每次比較都會產(chǎn)生當前的最小值,需要區(qū)分
  4. sorted: 已完成排序,每輪內(nèi)循環(huán)結束并完成交換后,當前外循環(huán)選定的值已完成排序

將以上幾種狀態(tài)對應到不同的 CSS 樣式,之后渲染的時候只需要通過操作樣式表即可實現(xiàn)不同狀態(tài)的標識:

/**
 * 渲染每一步
 * @param main 外循環(huán)的主數(shù)
 * @param div 被比較的用于操作的 div
 * @param state 操作涉及的樣式名稱
 * @param on 添加或去除樣式、
 */
function renderSelectionDiv(main, div, state, on){
    var onDiv = document.getElementById(div.toString());
    if(on == 1){
        if (state == ""){
            //    交換
            var outerDiv = document.getElementById(main.toString());
            swap(outerDiv, onDiv);
        }else{
            onDiv.classList.add(state);
        }
    }else {
        onDiv.classList.remove(state);
    }
}

Array 排序分解

之所以先寫上面的部分,是為了便于理解。實際過程中,我是先從排序時的循環(huán)過程開始嘗試,然后思考每幀需要的數(shù)據(jù),之后再回過頭來修正這一部分中的數(shù)據(jù)格式。

考慮排序過程中的兩層循環(huán),重點注意下標的變化過程:

  • 外循環(huán)是從 0數(shù)組倒數(shù)第二位
  • 內(nèi)循環(huán)是從 此輪外循環(huán)的值數(shù)組最后一位

理解循環(huán)的過程不難,較為繁瑣的是理清每一步所需的操作是什么,將上一部分中所設想的樣式變換插入到排序的循環(huán)中,下面是我的代碼:

/**
 * 提供外層循環(huán)的 selected 值,及內(nèi)層循環(huán)數(shù)組
 * @param outerId
 * @param innerQueue
 * @param timerQueue
 */
function getMin(outerId, innerQueue, timerQueue){
    var outerDiv = data[outerId];
    var minId = outerId;
  
    // 選中外層循環(huán)主值
    timerQueue.push([outerId, outerId, "select", 1]);

    while(innerQueue.length > 0){
        // 將需要比較的數(shù)存入數(shù)列
        var innerId = innerQueue.shift();

        var minDiv = data[minId];
        var innerDiv = data[innerId];

        timerQueue.push([outerId, innerId, "on", 1]);

        if(minDiv > innerDiv){
            // 修改最小值
            timerQueue.push([outerId, minId, "min", 0]);
            timerQueue.push([outerId, minId, "on", 0]);

            minId = innerId;

            timerQueue.push([outerId, minId, "min", 1]);

        }else {
            timerQueue.push([outerId, innerId, "on", 0]);
        }
    }

    // 交換
    timerQueue.push([outerId, minId, "", 1]);
    swapData(outerId, minId);
    // 去除最小值標識
    timerQueue.push([outerId, minId, "min", 0]);
    timerQueue.push([outerId, minId, "on", 0]);
    // 已排序標識
    timerQueue.push([outerId, outerId, "sort", 1]);
    // 去除選中值標識
    timerQueue.push([outerId, outerId, "select", 0]);
    timerQueue.push([outerId, outerId, "min", 0]);
    
    return timerQueue;
}

這里很簡單,但忍不住想要打一個比喻。

想象我們在為電影寫劇本,需要提前寫好每個鏡頭所涉及到的演員和表演場景,演員安排即是 Array 操作部分進行管理,而具體的場景則是 DOM 操作部分。

寫好劇本之后,交給 setTimeOut ,它將按照劇本的要求,召集人員進行拍攝,然后提供給瀏覽器播放出來。

如果上面我說得不是很清楚,那么帶著這樣的想象,回去思考這個過程,會很容易明白。歡迎批評指正。

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,797評論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,179評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,628評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,642評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,444評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,948評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,040評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,185評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,717評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,794評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,418評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,414評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,750評論 2 370

推薦閱讀更多精彩內(nèi)容