發現算法之美-排序

  • 什麼是排序?前端

    • 初識
    • 算法圖
  • JavaScript中的排序git

    • 普通排序
    • 複雜排序
    • 複雜排序函數封裝
    • lodash(v4.17.15)排序函數
    • 從V8源碼看sort()
  • 必會經典排序算法程序員

    • 冒泡排序(最大值置尾排序)
    • 選擇排序(最小值置頭排序)
    • 插入排序(尋找位置排序)
    • 歸併排序(二分遞歸排序)
    • 快速排序(基分遞歸排序)
  • leetcode 排序 解法題目github

    • 35.搜索插入位置(easy)
    • 88.合併兩個有序數組(easy)
    • 191.位1的個數(easy)
    • 581.最短無序連續子數組(easy)
    • 1331.數組序號轉換(easy)
    • 56.合併區間(medium)
    • 215.數組中的第K個最大元素(medium)
  • 參考資料

什麼是排序?

初識

生活中也有不少排序,好比考試之後按總分進行降序排列:
第一名700分、第二名699分、第三名698分等等等等
值得注意的是,雖然分數是倒序,可是名次倒是正序1,2,3···算法

排序在生活中的例子實在太多了,就不一一贅述了。數據庫

  • 排序的英文名爲sort
  • 排序是一個將無序(亂)的一組數據變爲有序的過程
  • 有序一般分爲兩種:升序(asc)和降序(desc)
  • 排序在軟件開發中很是常見:前端數據排序、後端數據庫查表升序(asc)、降序(desc)
  • 不少算法依賴於排序算法:棧式算法、二分查找法等等

算法圖

無序

image

降序

image

升序

image

JavaScript中的排序

在js中,Array.prototype上的sort()函數能夠很方便的達到咱們對升序和降序的要求。segmentfault

  • sort()能夠升序也能夠降序
  • sort()排序後,數組自己發生變化,不產生新數組

普通排序

假設要給[2,4,3,1,5]進行排序:後端

const arr = [2,4,3,1,5]
// 升序
arr.sort((a,b)=>a-b)
// 降序
arr.sort((a,b)=>b-a)

複雜排序

對於複雜狀況的話,例如須要對對象數組根據對象中的某個key排序。數組

// items按照value排序
const items = [
  { name: 'Edward', value: 21 },
  { name: 'Sharpe', value: 37 },
  { name: 'And', value: 45 },
  { name: 'The', value: -12 },
  { name: 'Magnetic', value: 13 },
  { name: 'Zeros', value: 37 }
];
items.sort((a, b) => a.value - b.value);

複雜排序函數封裝

// key 須要排序的key
// 升序仍是降序
function sortBy(arr, key, type = 'asc'){
   if(!arr || !key) return;
   let callback = (a, b) => a[key]- b[key] : 
   if( type === 'desc'){
       callback = (a, b) => b[key]- a[key] ;
   }
   arr.sort(callback);
}

lodash(v4.17.15)排序函數

  • _.sortedIndex(array, value)
  • _.sortedIndexBy(array, value, [iteratee=_.identity])
  • _.sortedIndexOf(array, value)
  • _.sortedUniq(array)
  • _.sortedUniqBy(array, [iteratee])

_.sortBy(collection, [iteratees=[_.identity]])瀏覽器

插入位置
_.sortedIndex([30, 50], 40); // => 1
複雜插入位置
var objects = [{ 'x': 4 }, { 'x': 5 }];
 
_.sortedIndexBy(objects, { 'x': 4 }, function(o) { return o.x; });
// => 0
 
// The `_.property` iteratee shorthand.
_.sortedIndexBy(objects, { 'x': 4 }, 'x');
// => 0
查詢第一個索引
_.sortedIndexOf([4, 5, 5, 5, 6], 5); // => 1
去重並排序
_.sortedUniq([1, 1, 2]); // => [1, 2]
複雜去重並排序
_.sortedUniqBy([1.1, 1.2, 2.3, 2.4], Math.floor);// => [1.1, 2.3]
根據某個key排序
var users = [
  { 'user': 'fred',   'age': 48 },
  { 'user': 'barney', 'age': 36 },
  { 'user': 'fred',   'age': 40 },
  { 'user': 'barney', 'age': 34 }
];
 
_.sortBy(users, [function(o) { return o.user; }]);
// => objects for [['barney', 36], ['barney', 34], ['fred', 48], ['fred', 40]]
 
_.sortBy(users, ['user', 'age']);
// => objects for [['barney', 34], ['barney', 36], ['fred', 40], ['fred', 48]]

從V8源碼看sort()

  • V8源碼內部的排序函數叫作InnerArraySort
  • InnerArraySort排序算法不只僅是一種通過多種優化的排序算法
  • InnerArraySort排序算法綜合運用到了快速排序和插入排序

    • 對於數組長度小於22的數組,運用插入排序
    • 對於數組長度大於等於22的數組,運用快速排序
function InnerArraySort(array, length, comparefn) {
  // In-place QuickSort algorithm.
  // For short (length <= 22) arrays, insertion sort is used for efficiency.
  var InsertionSort = function InsertionSort(a, from, to) {
    for (var i = from + 1; i < to; i++) {
      var element = a[i];
      for (var j = i - 1; j >= from; j--) {
        var tmp = a[j];
        var order = comparefn(tmp, element);
        if (order > 0) {
          a[j + 1] = tmp;
        } else {
          break;
        }
      }
      a[j + 1] = element;
    }
  };

  var QuickSort = function QuickSort(a, from, to) {
    var third_index = 0;
    while (true) {
      // Insertion sort is faster for short arrays.
      if (to - from <= 10) {
        InsertionSort(a, from, to);
        return;
      }
      if (to - from > 1000) {
        third_index = GetThirdIndex(a, from, to);
      } else {
        third_index = from + ((to - from) >> 1);
      }
      // Find a pivot as the median of first, last and middle element.
      var v0 = a[from];
      var v1 = a[to - 1];
      var v2 = a[third_index];
      var c01 = comparefn(v0, v1);
      if (c01 > 0) {
        // v1 < v0, so swap them.
        var tmp = v0;
        v0 = v1;
        v1 = tmp;
      } // v0 <= v1.
      var c02 = comparefn(v0, v2);
      if (c02 >= 0) {
        // v2 <= v0 <= v1.
        var tmp = v0;
        v0 = v2;
        v2 = v1;
        v1 = tmp;
      } else {
        // v0 <= v1 && v0 < v2
        var c12 = comparefn(v1, v2);
        if (c12 > 0) {
          // v0 <= v2 < v1
          var tmp = v1;
          v1 = v2;
          v2 = tmp;
        }
      }
      // v0 <= v1 <= v2
      a[from] = v0;
      a[to - 1] = v2;
      var pivot = v1;
      var low_end = from + 1;   // Upper bound of elements lower than pivot.
      var high_start = to - 1;  // Lower bound of elements greater than pivot.
      a[third_index] = a[low_end];
      a[low_end] = pivot;

      // From low_end to i are elements equal to pivot.
      // From i to high_start are elements that haven't been compared yet.
      partition: for (var i = low_end + 1; i < high_start; i++) {
        var element = a[i];
        var order = comparefn(element, pivot);
        if (order < 0) {
          a[i] = a[low_end];
          a[low_end] = element;
          low_end++;
        } else if (order > 0) {
          do {
            high_start--;
            if (high_start == i) break partition;
            var top_elem = a[high_start];
            order = comparefn(top_elem, pivot);
          } while (order > 0);
          a[i] = a[high_start];
          a[high_start] = element;
          if (order < 0) {
            element = a[i];
            a[i] = a[low_end];
            a[low_end] = element;
            low_end++;
          }
        }
      }
      if (to - high_start < low_end - from) {
        QuickSort(a, high_start, to);
        to = low_end;
      } else {
        QuickSort(a, from, low_end);
        from = high_start;
      }
    }
  };

  if (length < 2) return array;
  QuickSort(array, 0, num_non_undefined);
  return array;
}

必會經典排序算法

image

經典排序算法有十(幾)種,因爲當前的能力有限,我將先介紹冒泡、選擇、插入、歸併和快排這五種排序算法。

看了sort()函數的V8源碼之後,是否是束手無策以爲「哇 好難」,除了心存敬畏,其實明白算法是會通過不斷的優化的,sort()函數處理了根據JavaScript語言特性作了不少性能上的優化。
一般來講咱們去開心使用這樣性能又好使用又便捷的sort()函數便可,但其實有一些經典的排序算法,仍是很是值得去探索一下的。
即便數據結構課上學過,但其實時間一久,磚搬得過多,仍是容易忘記的,就算沒有忘記,工做幾年之後再回過頭來看算法,可能會對過去的算法作一個優化。

LeetCode的912題是一個很好的oj環境,適合對本身的排序算法作驗證,推薦給你們。

題目:https://leetcode-cn.com/probl...
題解:https://github.com/FrankKai/l...

  • 冒泡排序(最大值置尾排序)
  • 選擇排序(最小值置頭排序)
  • 插入排序(尋找位置排序)
  • 歸併排序(二分遞歸排序)
  • 快速排序(基分遞歸排序)

冒泡排序(最大值置尾排序)

/**
   * 解法:冒泡排序
   * 思路:外層每次循環都是不斷將最大值置於尾部,最小值像氣泡同樣向前冒出
   * 性能:4704ms 39.4MB
   * 時間複雜度:O(n^2)
   */
var sortArray = function (nums) {
  for (let i = 0; i < nums.length; i++) {
    for (let j = 0; j < nums.length - 1 - i; j++) {
      if (nums[j] > nums[j + 1]) {
        const temp = nums[j];
        nums[j] = nums[j + 1];
        nums[j + 1] = temp;
      }
    }
  }
  return nums;
}

選擇排序(最小值置頭排序)

/**
   * 解法:選擇排序
   * 思路:已排序區間和未排序區間。在未排序區間中找到最小數,與未排序區間的第一項(已排序區間的下一項)交換,將已排序區間從[]構形成[...],最終完成排序。如果降序的話,則找最大的數。
   * 性能:2104ms 41.5MB
   * 時間複雜度:O(n^2)
   */
var sortArray = function (nums) {
  for (let i = 0; i < nums.length; i++) {
    let min = nums[i];
    let idx = i;
    for (let j = i + 1; j < nums.length; j++) {
      if (nums[j] < min) {
        min = nums[j];
        idx = j;
      }
      if (j === nums.length - 1) {
        let temp = nums[i];
        nums[i] = nums[idx];
        nums[idx] = temp;
      }
    }
  }
  return nums;
}

插入排序(尋找位置排序)

/**
   * 解法:插入排序
   * 思路:已排序區間和未排序區間。取出未排序區間的第一項,在已排序區間上找到本身的位置,通常來講是找foo<x<bar,將x插入foo和bar之間,或者是x<bar插入頭部。
   * 關鍵點:插入到指定位置後當即中止在已排序數組中查找
   * 性能:2008ms 43.9MB
   * 時間複雜度:O(n^2)
   * */
var sortArray = function (nums) {
  const sorted = [nums[0]];
  for (let i = 1; i < nums.length; i++) {
    // j = i - 1; 也行
    for (let j = sorted.length - 1; j >= 0; j--) {
      if (nums[i] < sorted[j]) {
        if (j === 0) {
          sorted.splice(j, 0, nums[i]);
        }
      } else if (nums[i] >= sorted[j]) {
        sorted.splice(j + 1, 0, nums[i]);
        j = -1; // 這裏很關鍵,插入到指定位置後當即中止在已排序數組中查找
      }
    }
  }
  return sorted;
}
  /**
   * 優化版:插入排序(不借助輔助數組)
   * 思路:插入splice(j/j+1, 0), 刪除splice(i, 1)[0]
   * 須要注意的是: splice()返回的是一個數組,例如[1]
   * 性能:2372ms 42.5MB
   * 時間複雜度:O(n^2)
   */
var sortArray = function (nums) {
  for (let i = 1; i < nums.length; i++) {
    for (let j = i - 1; j >= 0; j--) {
      if (nums[i] < nums[j]) {
        if (j === 0) {
          nums.splice(j, 0, nums.splice(i, 1)[0]);
        }
      } else if (nums[i] >= nums[j]) {
        nums.splice(j + 1, 0, nums.splice(i, 1)[0]);
        j = -1;
      }
    }
  }
  return nums;
}

歸併排序(二分遞歸排序)

/**
   * 解法:歸併排序
   * 思路:將長度爲n的數組拆爲n/2長度的數組,分別對各自進行排序。再將n/2長度的數組使用歸併排序,直到最終的排序的數組長度爲2,最後將最終排序的數組依次向上合併
   * 核心:二分和遞歸。相似二分排序,自頂向下二分拆解排序,自底向上合併排序結果。
   * 注意:終止遞歸的條件爲if (length <= 1) { return nums; }
   * 性能:260ms 47.9MB
   * 時間複雜度: O(nlogn)
   */
var sortArray = function (nums) {
  const merge = (left, right) => {
    const result = [];
    while (left.length && right.length) {
      if (left[0] >= right[0]) {
        result.push(right.shift());
      } else {
        result.push(left.shift());
      }
    }
    while (left.length) {
      result.push(left.shift());
    }
    while (right.length) {
      result.push(right.shift());
    }
    return result;
  };
  let length = nums.length;
  if (length <= 1) {
    return nums;
  }
  let middle = Math.floor(length / 2);
  let left = nums.splice(0, middle);
  let right = nums;
  return merge(sortArray(left), sortArray(right));
}

快速排序(基分遞歸排序)

/**解法:快速排序
   * 思路:
   * 1.選中一個分割點split
   * 2.定義左右雙指針,一次遍歷將分割值小的置於左側,比分割值大的置於右側
   * 2.1 左右指針不相遇時 swap(left, right)
   * 2.2 左右指針相遇時,swap(start, left)而且返回left
   * 3.分治遞歸式爲左右兩側序列***
   * 性能:128ms 40.8MB
   * 時間複雜度:O(nlogn)
   */
var sortArray = function (nums) {
  quickSort(nums, 0, nums.length - 1);
  return nums;
  // 定義一個***函數
  function quickSort(arr, left, right) {
    if (left < right) {
      let splitIndex = findSplitIndex(nums, left, right);
      quickSort(nums, left, splitIndex - 1);
      quickSort(nums, splitIndex + 1, right);
    }
  }
  // 查找分割值索引
  function findSplitIndex(arr, left, right) {
    const start = left;
    const splitValue = arr[start];
    while (left !== right) {
      while (left < right && arr[right] > splitValue) {
        right--;
      }
      while (left < right && arr[left] <= splitValue) {
        left++;
      }
      if (left < right) {
        swap(arr, left, right);
      }
    }
    swap(arr, start, left);
    return left;
  }
  // 交換位置:左右交換、分割點與left交換
  function swap(arr, i, j) {
    const temp = arr[j];
    arr[j] = arr[i];
    arr[i] = temp;
  }
};

算法過程圖(來自程序員小灰的文章:漫畫:什麼是快速排序?(完整版)

image

image

image
image

image

image

image
image
image

leetcode 排序 解法題目

  • 35.搜索插入位置(easy)
  • 88.合併兩個有序數組(easy)
  • 191.位1的個數(easy)
  • 581.最短無序連續子數組(easy)
  • 1331.數組序號轉換(easy)
  • 56.合併區間(medium)
  • 215.數組中的第K個最大元素(medium)

35. 搜索插入位置

題目:https://leetcode-cn.com/probl...
題解:https://github.com/FrankKai/l...

var searchInsert = function(nums, target) {
 /**
   * 解法2:推入數組重排序法 96ms better than 6.35%
   */
  nums.push(target);
  var resortedNums = nums.sort((x,y)=>x-y);
  return resortedNums.indexOf(target);
};

88.合併兩個有序數組(easy)

題目:https://leetcode-cn.com/probl...
題解:https://github.com/FrankKai/l...

var merge = function(nums1, m, nums2, n) {
  /**
   * 特別須要注意的點:這道題會檢查nums1數組內存空間最後的存儲狀況
   */
  // splice截斷數組
  nums1.splice(m);
  nums2.splice(n);
  // 未使用concat的緣由:concat返回一個新數組,而題目須要直接在nums1的空間進行存儲
  nums2.forEach(num2 => {
    nums1.push(num2);
  });
  // sort排序當前數組
  var ascArr = nums1.sort((a, b) => a - b);
  return ascArr;
};

191. 位1的個數(easy)

題目:https://leetcode-cn.com/probl...
題解:https://github.com/FrankKai/l...

/**
 * @param {number} n - a positive integer
 * @return {number}
 */
var hammingWeight = function (n) {
  /**解法4:排序優化count
   * 性能:88ms 35.7MB
   */
  let strArr = n.toString(2).split("");
  strArr.sort((a, b) => parseInt(b) - parseInt(a));
  let count = 0;
  for (let i = 0; i < strArr.length; i++) {
    if (strArr[i] === "1") count++;
  }
  return count;
};

581.最短無序連續子數組(easy)

題目:https://leetcode-cn.com/probl...
題解:https://github.com/FrankKai/l...

/**
 * @param {number[]} nums
 * @return {number}
 */
var findUnsortedSubarray = function (nums) {
  /**
   * 解法
   * - 克隆數組並排序
   * - 找起始元素的索引值
   *     - startIdx 從頭至尾 找到第一個發生變化的元素索引
   *     - endIdx 從尾到頭 找到第一個發生變化的元素索引
   */
  // 使用[...nums]克隆一個新數組,是由於sort改變的是自身,不會返回一個新數組
  var sortedNums = [...nums].sort((a, b) => a - b);
  var startIdx = 0;
  for (var i = 0; i < nums.length; i++) {
    if (nums[i] !== sortedNums[i]) {
      startIdx = i;
      break;
    }
  }
  var endIdx = 0;

  for (var j = nums.length - 1; j >= 0; j--) {
    if (nums[j] !== sortedNums[j]) {
      endIdx = j;
      break;
    }
  }
  var length = endIdx - startIdx > 0 ? endIdx - startIdx + 1 : 0;
  return length;
};

1331. 數組序號轉換(easy)

題目:https://leetcode-cn.com/probl...
題解:https://github.com/FrankKai/l...

/**
 * @param {number[]} arr
 * @return {number[]}
 */
var arrayRankTransform = function(arr) {
  if (arr.length > Math.pow(10, 5)) return;
  /**
   * 生成惟一排序Map
   */
  var uniqArr = Array.from(new Set(arr));
  var sortArr = uniqArr.sort((a, b) => a - b);
  // 構造出一個二維數組做爲Map構造器入參
  var twoDimArr = sortArr.map((num, idx) => [num, idx + 1]);
  var idxMap = new Map(twoDimArr);
  /**
   * Map中查找數字序號
   */
  var serialNums = [];
  for (var i = 0; i < arr.length; i++) {
    serialNums.push(idxMap.get(arr[i]));
  }
  return serialNums;
};

56.合併區間(medium)

題目:https://leetcode-cn.com/probl...
題解:https://github.com/FrankKai/l...

/**
 * @param {number[][]} intervals
 * @return {number[][]}
 */
var merge = function (intervals) {
  /**
   * 解法1:排序 + 棧
   * 性能:88ms 36.3MB
   * 思路:
   * 推入區間 空棧 或者 arr[0]大於棧最後一個區間閉
   * 覆蓋重疊 arr[0]小於棧最後一個區間閉
   *  */
  intervals.sort((a, b) => a[0] - b[0]);
  let stack = [];
  for (let i = 0; i < intervals.length; i++) {
    let currrentInterval = intervals[i];
    let stackLastItem = stack[stack.length - 1];
    if (stack.length === 0 || currrentInterval[0] > stackLastItem[1]) {
      stack.push(currrentInterval);
    } else if (currrentInterval[0] <= stackLastItem[1]) {
      stackLastItem[1] = Math.max(stackLastItem[1], currrentInterval[1]);
    }
  }
  return stack;
};

215. 數組中的第K個最大元素(medium)

題目:https://leetcode-cn.com/probl...
題解:https://github.com/FrankKai/l...

var findKthLargest = function (nums, k) {
  /**
   * 解法1:倒序排序輸出
   */
  nums.sort((a, b) => b - a);
  return nums[k - 1];
};

參考資料

期待和你們交流,共同進步,歡迎你們加入我建立的與前端開發密切相關的技術討論小組:

努力成爲優秀前端工程師!
相關文章
相關標籤/搜索