发现算法之美-排序

简介: 发现算法之美-排序

image.png


  • 什么是排序?
  • 初识
  • 算法图
  • JavaScript中的排序
  • 普通排序
  • 复杂排序
  • 复杂排序函数封装
  • lodash(v4.17.15)排序函数
  • 从V8源码看sort()
  • 必会经典排序算法
  • 冒泡排序(最大值置尾排序)
  • 选择排序(最小值置头排序)
  • 插入排序(寻找位置排序)
  • 归并排序(二分递归排序)
  • 快速排序(基分递归排序)
  • leetcode 排序 解法题目
  • 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)
  • 很多算法依赖于排序算法:栈式算法、二分查找法等等


算法图

无序


降序


升序


JavaScript中的排序


在js中,Array.prototype上的sort()函数可以很方便的达到我们对升序和降序的要求。

  • 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;
}


必会经典排序算法


经典排序算法有十(几)种,由于当前的能力有限,我将先介绍冒泡、选择、插入、归并和快排这五种排序算法。

看了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;
}


1460000022852470.gif

归并排序(二分递归排序)

  /**
   * 解法:归并排序
   * 思路:将长度为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));
}


1460000022852471.gif


快速排序(基分递归排序)

  /**解法:快速排序
   * 思路:
   * 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;
  }
};

算法过程图(来自程序员小灰的文章:漫画:什么是快速排序?(完整版)


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];
};


参考资料

相关文章
|
6月前
|
算法 C++
【洛谷 P1223】排队接水(贪心算法+结构体排序)
该问题要求安排$n$个人的排队顺序以最小化平均等待时间。每个人接水需时$T_i$,解决方案是让接水时间短的人优先。给定$n\leq1000$和$t_i\leq10^6$,代码示例使用C++实现,通过排序使时间从小到大排列,然后计算平均等待时间。样例输入为10个人的时间数组,输出为优化后的排队顺序及平均等待时间(291.90)。
53 0
|
4月前
|
算法
【算法】二分查找——在排序数组中查找元素的第一个和最后一个位置
【算法】二分查找——在排序数组中查找元素的第一个和最后一个位置
|
21天前
|
搜索推荐 算法 C语言
【排序算法】八大排序(上)(c语言实现)(附源码)
本文介绍了四种常见的排序算法:冒泡排序、选择排序、插入排序和希尔排序。通过具体的代码实现和测试数据,详细解释了每种算法的工作原理和性能特点。冒泡排序通过不断交换相邻元素来排序,选择排序通过选择最小元素进行交换,插入排序通过逐步插入元素到已排序部分,而希尔排序则是插入排序的改进版,通过预排序使数据更接近有序,从而提高效率。文章最后总结了这四种算法的空间和时间复杂度,以及它们的稳定性。
65 8
|
21天前
|
搜索推荐 算法 C语言
【排序算法】八大排序(下)(c语言实现)(附源码)
本文继续学习并实现了八大排序算法中的后四种:堆排序、快速排序、归并排序和计数排序。详细介绍了每种排序算法的原理、步骤和代码实现,并通过测试数据展示了它们的性能表现。堆排序利用堆的特性进行排序,快速排序通过递归和多种划分方法实现高效排序,归并排序通过分治法将问题分解后再合并,计数排序则通过统计每个元素的出现次数实现非比较排序。最后,文章还对比了这些排序算法在处理一百万个整形数据时的运行时间,帮助读者了解不同算法的优劣。
59 7
|
2月前
|
搜索推荐 Shell
解析排序算法:十大排序方法的工作原理与性能比较
解析排序算法:十大排序方法的工作原理与性能比较
56 9
|
2月前
|
算法 搜索推荐 Java
数据结构与算法学习十三:基数排序,以空间换时间的稳定式排序,速度很快。
基数排序是一种稳定的排序算法,通过将数字按位数切割并分配到不同的桶中,以空间换时间的方式实现快速排序,但占用内存较大,不适合含有负数的数组。
26 0
数据结构与算法学习十三:基数排序,以空间换时间的稳定式排序,速度很快。
|
2月前
|
算法
❤️算法笔记❤️-(每日一刷-83、删除排序链表中的重复项)
❤️算法笔记❤️-(每日一刷-83、删除排序链表中的重复项)
32 0
|
2月前
|
存储 算法 搜索推荐
算法进阶之路:Python 归并排序深度剖析,让数据排序变得艺术起来!
算法进阶之路:Python 归并排序深度剖析,让数据排序变得艺术起来!
74 0
|
4月前
|
搜索推荐 算法 Java
现有一个接口DataOperation定义了排序方法sort(int[])和查找方法search(int[],int),已知类QuickSort的quickSort(int[])方法实现了快速排序算法
该博客文章通过UML类图和Java源码示例,展示了如何使用适配器模式将QuickSort类和BinarySearch类的排序和查找功能适配到DataOperation接口中,实现算法的解耦和复用。
44 1
现有一个接口DataOperation定义了排序方法sort(int[])和查找方法search(int[],int),已知类QuickSort的quickSort(int[])方法实现了快速排序算法
|
4月前
|
算法 搜索推荐 Java
算法实战:手写归并排序,让复杂排序变简单!
归并排序是一种基于“分治法”的经典算法,通过递归分割和合并数组,实现O(n log n)的高效排序。本文将通过Java手写代码,详细讲解归并排序的原理及实现,帮助你快速掌握这一实用算法。
43 0