leetcode算法学习-排序

221 阅读15分钟

1.题目912. 排序数组

给你一个整数数组 nums,请你将该数组升序排列。

示例 1:

输入: nums = [5,2,3,1]
输出: [1,2,3,5]

示例 2:

输入: nums = [5,1,1,2,0,0]
输出: [0,0,1,1,2,5]

2.答题

1.冒泡排序

原理

  1. 从左到右边,依次比较左右两个元素的大小,如果左边比右边大,互换位置
  2. 一直比较到最后一个元素,这样一次循环下来,最大的一定在右边
  3. 第二次循环则只需要(遍历+判断替换)到最后一个的前一个,因为最后一个已经是确认最大的了
  4. 一直循环下去,直到左边已经没有可以循环比较的元素

image.png image.png

image.png

代码实现

/**
 * @param {number[]} nums
 * @return {number[]}
 * 
 */
//通用交替方法
 function swap(nums, i, j) {
    ;[nums[i], nums[j]] = [nums[j], nums[i]]
  }   
  
 // 冒泡排序1 每次找最大值
  //双循环,每一次循环 比较 j-1 与j的关系,是上一个与当前关系 
  const sortArray = nums => {
    const n = nums.length
    for (let i = n - 1; i > 0; i--) {//从尾巴开始,给j的总长度慢慢递减,
    //这里其实跟“冒泡排序2” 类似,只是i 作为了下面j的终止条件j <= i,而不是开始条件,最终j还是从下标0到大开始遍历
      for (let j = 1; j <= i; j++) { //总长度依次慢慢缩小为1,上一个j与当前j比较,
        if (nums[j - 1] > nums[j]) {
          swap(nums, j - 1, j)
        }
      }
    }
    return nums
  }
  
 // 冒泡排序2 每次找最小值
// 直接双循环,从左往右边比较 i和j的关系,是当前与下一个关系
var sortArray = function(nums) {
    for(let i = 0; i < nums.length -1; i ++) { //这里 最后一个i 只需要到倒数第二就ok,倒数第一 j++就没意义,[5,1,1,2,4,0] ,比如这里知道i只到4即可
        for(let j = i + 1; j < nums.length; j ++) { //j的起点随着i慢慢递增
            if(nums[i] > nums[j]) {
                let temp = nums[i]
                nums[i] = nums[j]
                nums[j] = temp
            }
        }
    }
    return nums
};

优化1 标记是否替换过

在每一次小循环时,如果当前对比没有替换动作,则后面的冒泡都可以省略。

 
var sortArray = function(nums) {
    for(let i = 0; i < nums.length -1; i ++) { 
    let isSorted = true //  是否排过序,默认是
        for(let j = 0; j < nums.length - i -1; j ++) { //j的起点随着i慢慢递增
            if(nums[i] > nums[j]) {
                let temp = nums[i]
                nums[i] = nums[j]
                nums[j] = temp
                isSorted = false // 有替换 让下一个比较继续
            }
        }
        if(isSorted) {
            //终止这次小循环
            break;
        }
    }
    
    return nums
};

优化2 设定边界值

通过记录最后一次边际替换值,来调整下一次小循环的边界长度。

 
var sortArray = function(nums) {
    let lastChangeIndex = 0
    let sortBorderLen = nums.length -1
    for(let i = 0; i < nums.length -1; i ++) { 
    let isSorted = true //  是否排过序,默认是
        for(let j = 0; j < sortBorderLen; j ++) { //j的起点随着i慢慢递增
            if(nums[i] > nums[j]) {
                let temp = nums[i]
                nums[i] = nums[j]
                nums[j] = temp
                isSorted = false // 有替换 让下一个比较继续
                lastChangeIndex = i //记录当前结束的边界
            }
        }
        sortBorderLen = lastChangeIndex //改变下一次小循环的长度
        if(isSorted) {
            //终止这次小循环
            break;
        }
    }
    
    return nums
};

优化3 鸡尾酒排序

鸡尾酒排序 : 双向冒泡排序,因为鸡尾酒就是多种酒混搭,不按常规出牌。

主要逻辑: 像一个大钟摆,从左到右边便利,然后再从右往左边便利,一旦发现所有都不需要替换就立马停止。


var s = [1,2,3,4,5,8,6,7];
var cocktailSort = function(array) {
    var count = 0; 
    var temp;
    for(var i = 0; i < array.length/2; i++) {
        var flag = false;  //定义flag
        for(var j = count; j < array.length - count -1; j++) {
            if(array[j] > array[j+1]) {
                temp = array[j];
                array[j] = array[j+1];
                array[j+1] = temp;
                flag = true;  //当发生交换时,改变flag的值为true
            }
        }
        count++;
        for(var k = array.length - 1 - count; k > count - 1; k--) {
            if(array[k] < array[k-1]) {
                temp = array[k];
                array[k] = array[k-1];
                array[k-1] = temp;
                flag = true;  //当发生交换时,改变flag的值true
            }
        }
        console.log(array);
        if(!flag)
            break;
    }
};
cocktailSort(s);
//=>[ 1, 2, 3, 4, 5, 6, 7, 8 ]  //输出结果显示,算法会提前结束
 

2.选择排序

原理

思路

选择排序算法的实现思路有点类似插入排序,也分已排序区间和未排序区间。但是选择排序每次会从未排序区间中找到最小的元素,将其放到已排序区间的末尾。

步骤

  1. 首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。(第一次每个元素都要遍历一遍比较)
  2. 再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
  3. 重复第二步,直到所有元素均排序完毕。

image.png

代码实现

  // 选择排序1 
 //1.从原序列中找到最小值,与数组第一个元素交换;
 //2.除第一个元素外,从剩下未排序的序列中找到最小值,与数组第二个元素交换;
 //3.共N-1趟,每趟都找到未排序的最小值,放到已排序的序列后面。 
  const sortArray = nums => {
    const n = nums.length
    for (let i = n - 1; i > 0; i--) {//从最后一个元素开始遍历,
      let minIndex = 0  //刚开始拿出第一个元素5,索引0与其他所有元素比较,[5,4,3,6]  
      for (let j = 0; j <= i; j++) {
        if (nums[j] < nums[minIndex]) minIndex = j //找到最小的那个值,把index替换到0的位置
      //第一次拿5跟5比较,
      //第二次4跟5比较 替换位置,minIndex = 1
      //第三次3跟4比较 替换位置,minIndex = 2
      //第三次6跟4比较 minIndex = 2 不变
      }
      swap(nums, i, maxIndex)//这里交换索引位置 0 和 2的位置
    }
    return nums
  }

3.插入排序

  • 插入排序的核心是插入,也是如同选择排序,将一个数组分为有序数组和无序数组。
  • 不同的是,他是将无序数组的第一位插入到有序数组的合适位置,并将该位置之后的有序数组统一往后移动一位,继续遍历无序数组直到无序数组长度为0。
  • 核心算法是找到有序数组中合适位置

image.png

代码实现

  // 插入排序 
  // 通过从左起(左边一直是有序的),依次在右边乱的数据拿出一个,插入到左边准确的位置。
// -InsertionSort 和打扑克牌时,从牌桌上逐一拿起扑克牌,在手上排序的进程相同。
// Input: {4, 3, 8, 5, 2, 6, 1, 7}。 
// 1.首先拿起第一张牌, 手上有 {4}。
// 2.拿起第二张牌 3, 把 3insert 到手上的牌 {4}, 得到 {3 ,4}。
// 3.拿起第三张牌 8, 把 8 insert 到手上的牌 {3,4 }, 得到 {3 ,4,8}。 
 const sortArray = nums => {
    for (let i = 1; i < nums.length; i++) {//i不断递增,
      let j = i
      while (j > 0 && nums[j - 1] > nums[j] ) {//j与上一个j 比较,长度判断从j=1,不断趋近于j=nums.length 
      // 由于左边已经是有序的,从小到大,所以左边最后一个nums[j - 1]是左边最大值,只需要判断nums[j - 1] 小于 nums[j] 才需要进行进一步判断替换
        swap(nums, j, j - 1)//这里开始替换
        j--
      }
    }
    return nums
  } 
  

4.归并排序

原理

少做事情的思想

简单版本

  • ,把数组一分为二,各自进行冒泡排序,让把两个排好的数组进行合并。
  • 合并的时候,由于左边依次从左边第一个,与右边的第一个比较,较小的拿出放到结果列表里。

image.png

  • 如上面的 2和6 ,取2。
  • 继续比较:4和6 ,取4。
  • 继续比较:19和6 ,取6。
  • 继续比较:19和10 ,取10。
  • 继续比较:19和13 ,取13。
  • 继续比较:19和20 ,取19。
  • 继续比较:40和20 ,取20。
  • 继续比较:最后一个40。

这里关键在于:由于左右两边都是排好序,最小的元素肯定是两个数组中其中一个。所以永远按两个元素的首元素比较取出,则永远都能拿到最小值。

升级版本

通过递归的方式,把数组继续拆分到单个不能再拆分。然后再回溯合并。 此过程叫做分治法(Divide and Conquer),也叫二路归并

  • 自上而下的递归
  • 自下而上的迭代 使用网上的流行图:

mergeSort.gif

依次拆分到最小元素如:(3,44) (38,5),然后开始 (小排序+小排序) 合并成一个新的有序数组

代码实现

  // 归并排序- 普通
   function merge(left,right){
     var temp=[];
    //这里要合并两个 分割的数组,如[4,5,7,8]和[1,2,3,6],最终要得到[1,2,3,4,5,6,7,8]
    //通过比较两个数组的头元素,如4和1,1比较小,1加入temp,移动,不断插入数据到临时数组 temp上面
     while(left.length&&right.length){
         if(left[0]<right[0]){
             temp.push(left.shift());
         }else{
             temp.push(right.shift());
         }
     }
     // 由于left,right本身是有序的, 直接连接剩下的长的部分
     return temp.concat(left,right); 
 }
 //用于拆分
 function arrSplitAndMerge(data){
     if(data.length<=1){
         return data;
     }
     var mid=Math.floor(data.length/2);
     var left=data.slice(0,mid);
     var right=data.slice(mid);

     return  merge(arrSplitAndMerge(left),arrSplitAndMerge(right));//递归拆分,拆分完进行 回溯合并
 }
 const sortArray = (nums) => {
    return arrSplitAndMerge(nums);
 } 
 
  // 归并排序 高效版-优化合并数组逻辑
 const sortArray = (nums, left = 0, right = nums.length - 1) => {
    if (left >= right) return nums // 当left = 0 直接返回单个数字
    const mid = (left + right) >> 1   //总长度除于2
  
    sortArray(nums, left, mid)
    sortArray(nums, mid + 1, right)
  
    let i = left
    let j = mid + 1
    let index = 0
    const arr = new Array(right - left + 1)
    //console.log( "nums",nums) 
    //这里要合并两个 分割的数组,如[4,5,7,8]和[1,2,3,6],最终要得到[1,2,3,4,5,6,7,8]
    //通过比较两个数组的头元素,移动i和j的游标,不断插入数据到临时数组 arr上面
    while (i <= mid && j <= right) {
      if (nums[i] <= nums[j]) arr[index++] = nums[i++]
      else arr[index++] = nums[j++]
    }
    //把剩下的数据都插到  arr上面
    while (i <= mid) arr[index++] = nums[i++]
    while (j <= right) arr[index++] = nums[j++]
    for (let k = 0; k < arr.length; k++) {//这里把 合并后的数组返回。由于是共用方法,需要区分合并的起点left位置
      nums[left + k] = arr[k]//这里不创建其他临时存储空间,直接把更新后的排序内容赋值到原来的索引上面
      //比如[8, 5, 10, 3, 2, 18, 17, 9],中拆分到 10, 3的时候,排序后是 3,10 赋值到 索引2,3的位置,所以循环从left+k开始
    }
    //console.log("left",left,"arr",arr,"nums",nums)
    return nums
  }
 //let dat=[8, 5, 10, 3, 2, 18, 17, 9];
 //console.log( sortArray(dat)) 
  

5.快速排序

原理

  1. 在一堆无序的数字里(最大为100)。挑选一个,比如51,这个51就是叫做枢值
  2. 把所有小于51的分为A组,所有大于等于51放B组。
  3. 依次继续再A组里面随机选一个值比如30做枢值(由于A组都是小于51,所有拿到的值一定在0-50之间)
  4. 则A被分解成:C组0-29,D组30到50。
  5. 同理 B 取出的是 70,则分成:E组51-69,F组70到100。
  6. 直到不能再细分组为止。

由于每次分组操作对下一次都是有意义的,没有做过多的无用功。

实现逻辑

  1. 双边循环法
  • 使用left 和right代表左右两端指针,每次与最左边的枢值pivot比较。(枢值也可以是随机不一定要最左边)
  • right先开始,当大于等于pivot往左边移动,否则停止,轮到left移动。
  • 当left小于等于pivot往右边移动,否则停止,到此一次循环结束。替换当前left和right对应的值。
  • 一直right和left移动,直到left和right重合,把left对应的值,和刚开始枢值pivot 替换。
  • 到此一次二分结束。

image.png

  1. 单边循环法
  • 以最左边元素做枢值pivot。(枢值也可以是随机不一定要最左边)
  • 设置一个mark指针 代表小雨基准元素的区域边界,位置也是从最左边元素开始往右边遍历。
  • 当遇到下一个元素大于枢值,就继续移动遍历,不移动mark。
  • 当遇到下一个元素小于枢值
  • 1.把mark往右边移动一位(留出1个空位的给小值放)
  • 2.把mark对应的值与下一个元素交换位置。
  • 然后一直这样遍历到数组结束,最后把枢值和mark值替换即可。

核心思想:mark是一个动态的中间值,遇到大的值丢mark的右边(默认就在右边),遇到小的值mark左边留个空位,丢到mark的左边。

image.png

image.png

代码实现

快速排序

//双指针版本
function quickSort(arr, begin, end) {
    //递归出口
    if(begin >= end)
        return;
    var l = begin; // 左指针
    var r = end; //右指针
    var temp = arr[begin]; //基准数,这里取数组第一个数
    //左右指针相遇的时候退出扫描循环
    while(l < r) {
        //右指针从右向左扫描,碰到第一个小于基准数的时候停住
        while(l < r && arr[r] >= temp)
            r --;
        //左指针从左向右扫描,碰到第一个大于基准数的时候停住
        while(l < r && arr[l] <= temp)
            l ++;
        //交换左右指针所停位置的数
        [arr[l], arr[r]] = [arr[r], arr[l]];
    }
    //最后交换基准数与指针相遇位置的数
    [arr[begin], arr[l]] = [arr[l], arr[begin]];
    //递归处理左右数组
    quickSort(arr, begin, l - 1);
    quickSort(arr, l + 1, end);
}

var arr = [2,3,4,1,5,6]
quickSort(arr, 0, 5);
console.log(arr)


//单指针 版本
    function quick_sort(list, start, end) {
      if (start < end) {
        var pivotpos = partition(list, start, end);   //找出快排的基数
        quick_sort(list, start, pivotpos - 1);        //将左边的快排一次
        quick_sort(list, pivotpos + 1, end);          //将右边的快排一次
      }
    }
    //将一个序列调整成以基数为分割的两个区域,一边全都不小于基数,一边全都不大于基数
    function partition(list, start, end) {
      var pivotpos = start;
      var pivot = list[start];
      var tmp;
      for(var i = start + 1; i <= end; i ++) {
        if (list[i] < pivot) {
          tmp = list[i];
          pivotpos += 1;
          list[i] = list[pivotpos];
          list[pivotpos] = tmp;
        }
      }

      tmp = list[start];
      list[start] = list[pivotpos];
      list[pivotpos] = tmp;
      return pivotpos;
    }
//测试代码
    var list = [8,2,4,65,2,4,7,1,9,0,2,34,12];
    quick_sort(list, 0, list.length);



  // 双指针 往里走版 升级版
  const sortArray = (nums, left = 0, right = nums.length - 1) => {
    if (left >= right) return nums
    let i = left
    let j = right - 1
    while (i <= j) {
      if (nums[i] > nums[right]) {
        ;[nums[i], nums[j]] = [nums[j], nums[i]]
        j--
      } else {
        i++
      }
    }
    j++
    ;[nums[j], nums[right]] = [nums[right], nums[j]]
    sortArray(nums, left, j - 1)
    sortArray(nums, j + 1, right)
    return nums
  }
  
  // 快速排序 易懂版+效率也高 取中间值,拼接
   function sortArray(arr){
    //如果数组只有一个数,就直接返回;
    if(arr.length<1){
      return arr;  
    } 
    //找到中间的那个数的索引值;如果是浮点数,就向下取整
    var centerIndex = Math.floor(arr.length/2);
    //根据这个中间的数的索引值,找到这个数的值;
    var centerNum = arr.splice(centerIndex,1);
    //存放左边的数
    var arrLeft = [];
    //存放右边的数
    var arrRight = [];
    for(i=0;i<arr.length;i++){
      if(arr[i]<=centerNum){
        arrLeft.push(arr[i])
      }else if(arr[i]>centerNum){
        arrRight.push(arr[i])
      }
    }
    return sortArray(arrLeft).concat(centerNum,sortArray(arrRight));    
  };
  

6.桶排序

原理

  1. 先分桶,划分合适数量的桶。
  2. 将所有待排序数据放入到对应的桶中。
  3. 使用合理的算法对每个非空桶进行子排序。
  4. 按顺序将每个桶中数据进行合并。

image.png

image.png

代码实现

  // 桶排序
  const sortArray = nums => {
    const each = 100    // 每个桶的范围
    let min = Number.MAX_SAFE_INTEGER
    let max = Number.MIN_SAFE_INTEGER
    for (const num of nums) {
      num < min && (min = num)
      num > max && (max = num)
    }
    const count = max - min + 1
    const bucketCount = count % each === 0 ? count / each : Math.floor(count / each) + 1
  
    const buckets = new Array(bucketCount)
    for (const num of nums) {
      const cur = Math.floor((num - min) / each)
      if (!buckets[cur]) buckets[cur] = []
      buckets[cur].push(num)
    }
  
    let index = 0
    for (const bucket of buckets) {
      if (!bucket) continue
      // 对每个桶内的元素排序,直接使用 sort
      bucket.sort((pre, next) => pre - next)
      for (const num of bucket) {
        nums[index++] = num
      }
    }
    return nums
  } 
  

7.堆排序

原理

(二叉)堆:是一个数组,可以看成是一个完全二叉树。

  • 堆是一个完全二叉树。 完全二叉树:除了最后一层,其他层的节点个数都是满的,最后一层的节点都靠左排列。
  • 堆中每一个节点的值都必须大于等于(或小于等于)其子树中每个节点的值。 也可以说:堆中每个节点的值都大于等于(或者小于等于)其左右子节点的值。这两种表述是等价的。

最大堆:除了根节点以外的所有结点,其结点的值小于等于其父节点,A[parent(i)] >= A[i]。因此堆中最大元素存放于根结点中。

image.png 最小堆:除了根节点以外的所有结点,其结点的值大于等于其父节点,A[parent(i)] <= A[i]。因此堆中最小元素存放于根结点中。

image.png

完全二叉树有个特性:左边子节点位置 = 当前父节点的两倍 + 1右边子节点位置 = 当前父节点的两倍 + 2

简单来说: 堆其实可以用一个数组表示,给定一个节点的下标 i (i从1开始) ,那么它的父节点一定为 A[i/2] ,左子节点为 A[2i] ,右子节点为 A[2i+1]

  • i 结点的父结点 par = floor((i-1)/2) 「向下取整」
  • i 结点的左子结点 2 * i + 1
  • i 结点的右子结点 2 * i + 2

实现逻辑

利用删除堆顶会导致堆重现排列的特性,并且删除的堆顶就是最大值,刚好插在末尾节点上。 依次删除所有的堆顶。结束时候就是有序的。是一个下沉的过程。

  1. 先把无序的数组构建成二叉堆。利用最大堆解决从小到大排序。利用最小堆解决从大到小排序。
  2. 循环删除堆顶,替换到二叉堆的末尾,并且调整堆产生新的堆。

代码实现

 // 堆排序 详细注释版本
    const sortArray = array => {///////‘
	// 1. 初始化大顶堆,从第一个非叶子结点开始,把无序的数组变成最大堆
	for (let i = Math.floor(array.length / 2 - 1); i >= 0; i--) {
		heapify(array, i, array.length);//下沉
	}
	// 2. 排序,每一次 for 循环找出一个当前最大值,数组长度减一
        // 循环删除堆顶的元素,移到集合的末端,在调整堆变成 新的堆顶
	for (let i = Math.floor(array.length - 1); i > 0; i--) {
		// 根节点与最后一个节点交换 相当于顶堆删除动作
		swap(array, 0, i);
		// 从根节点开始调整,并且最后一个结点已经为当前最大值,不需要再参与比较,所以第三个参数为 i,即比较到最后一个结点前一个即可
		heapify(array, 0, i);
	}
	return array;
};
    // 交换两个节点
    const swap = (array, i, j) => {
            let temp = array[i];
            array[i] = array[j];
            array[j] = temp;
    };
    
    // 下沉方法
    // 将 i 结点以下的堆整理为大顶堆,注意这一步实现的基础实际上是:
    // 假设结点 i 以下的子堆已经是一个大顶堆,heapify 函数实现的
    // 功能是实际上是:找到 结点 i 在包括结点 i 的堆中的正确位置。
    // 后面将写一个 for 循环,从第一个非叶子结点开始,对每一个非叶子结点
    // 都执行 heapify 操作,所以就满足了结点 i 以下的子堆已经是一大顶堆
    const heapify = (array, i, length) => {
	let temp = array[i]; // 当前父节点
	// j < length 的目的是对结点 i 以下的结点全部做顺序调整
	for (let j = 2 * i + 1; j < length; j = 2 * j + 1) {
		temp = array[i]; // 将 array[i] 取出,整个过程相当于找到 array[i] 应处于的位置
		if (j + 1 < length && array[j] < array[j + 1]) {
			j++; // 找到两个孩子中较大的一个,再与父节点比较
		}
		if (temp < array[j]) {
			swap(array, i, j); // 如果父节点小于子节点:交换;否则跳出
			i = j; // 交换后,temp 的下标变为 j
		} else {
			break;
		}
	}
    }; 
    //const array = [4, 6, 8, 5, 9, 1, 2, 5, 3, 2];
    //console.log('原始array:', array);
    //const newArr = sortArray(array);
    //console.log('newArr:', newArr);
  
  // 堆排序 方案2
  const sortArray = nums => {
    heapify(nums)
    for (let i = nums.length - 1; i > 0; i--) {
      swap(nums, i, 0)
      rebuildHeap(nums, 0, i - 1)
    }
  
    // 数组转成最大堆
    function heapify(nums) {
      for (let i = 1; i < nums.length; i++) {
        let parent = (i - 1) >> 1
        let child = i
        while (child > 0 && nums[child] > nums[parent]) {
          swap(nums, parent, child)
          child = parent
          parent = (parent - 1) >> 1
        }
      }
    }
  
    function rebuildHeap(nums, parent, last) {
      const left = 2 * parent + 1
      const right = 2 * parent + 2
      let maxIndex = left
      if (right <= last && nums[right] > nums[left]) {
        maxIndex = right
      }
  
      if (maxIndex <= last && nums[maxIndex] > nums[parent]) {
        swap(nums, maxIndex, parent)
        rebuildHeap(nums, maxIndex, last)
      }
    }
    return nums
  }

8.计数排序

原理

计数排序的核心在于将数据值转化为数组的下标、然后利用数组下标天然有序的特征。

缺点:如果是有很大数字10000000,会占用很多空间

算法步骤

  • 找出待排序的数组中最大和最小的元素
  • 统计数组中每个值为i的元素出现的次数,存入数组C 的第i项
  • 对所有的计数累加 (从C 中的第一个元素开始,每一项和前一项相加)

1.创建最大数组

image.png

2.把每个值放到数组里 对应的index里,重复就累加

image.png

3.最后遍历数组依次取出

image.png

代码实现

计数排序

  // 计数排序
  const sortArray = nums => {
    let min = Number.MAX_SAFE_INTEGER
    let max = Number.MIN_SAFE_INTEGER
    for (const num of nums) {
      num < min && (min = num)
      num > max && (max = num)
    }
  
    const count = new Array(max - min + 1).fill(0)//这里max - min 来优化总长度,当要访问的时候 使用num - min做实际访问的下标。
    for (const num of nums) {
      count[num - min]++ 
    }
  
    let index = 0
    for (let i = 0; i < count.length; i++) {
      while (count[i] > 0) {
        nums[index++] = i + min
        count[i]--
      }
    }
    return nums
  }

9.希尔排序

希尔排序是插入排序的改进版本,也成为缩小增量排序(Diminishing Increment Sort)。 

原理:

  1. 将待排序的数组元素按下标的一定增量分组,将其分成多个子序列
  2. 对各子序列进行插入排序
  3. 依次缩减增量重复执行排序操作
  4. 直至增量缩小为1时进行最后一次插入排序
  • 希尔排序增量(希尔增量)范围:[1, 待排序数组长度)
  • 取值一般从 待排序数组长度一半 开始
  • 后续每次减半,直至增量为1

以长度为9的待排序数组为例:

  • 第一个增量: 9 / 2 = 4
  • 第二个增量: 4 / 2 = 2
  • 第三个增量: 2 / 2 = 1

image.png

代码实现

  const sortArray = nums => {
    const n = nums.length
    let gap = n >> 1
    while (gap > 0) {
      for (let i = 0; i < gap; i++) {
        for (let j = i + gap; j < n; j += gap) {
            let temp = j
            while (temp > i && nums[temp] < nums[temp - gap]) {
                swap(nums, temp, temp - gap)
                temp -= gap
            }
        }
      }
      gap >>= 1
    }
    return nums
  }

10 基数排列

原理

主要思想:将整数按位数切割成不同的数字,然后按每个位数分别比较从而得到有序的序列。

步骤:

  1. 将所有待比较数值(正整数)统一为同样的数位长度,数位较短的数前面补零。
  2. 创建 0- 9 的10位数组。
  3. 依次从把数字的第一位匹配 数组位置,并放入
  4. 把数据的数字,从左到有依次拿出,成为新数组
  5. 然后重复3的步骤,只是规则改成从第二位开始
  6. 直到最前面一位也放完和取出。得到最终的数组

缺点:如果数字位数很大,效率很低,适合位数少的数组。 image.png

image.png

代码实现


//LSD Radix Sort
var counter = [];
function radixSort(arr, maxDigit) {
    var mod = 10;
    var dev = 1;
    for (var i = 0; i < maxDigit; i++, dev *= 10, mod *= 10) {
        for(var j = 0; j < arr.length; j++) {
            var bucket = parseInt((arr[j] % mod) / dev);
            if(counter[bucket]==null) {
                counter[bucket] = [];
            }
            counter[bucket].push(arr[j]);
        }
        var pos = 0;
        for(var j = 0; j < counter.length; j++) {
            var value = null;
            if(counter[j]!=null) {
                while ((value = counter[j].shift()) != null) {
                      arr[pos++] = value;
                }
          }
        }
    }
    return arr;
}

刷题

剑指 Offer 45. 把数组排成最小的数

输入一个非负整数数组,把数组里所有数字拼接起来排成一个数,打印能拼接出的所有数字中最小的一个。

示例 1:

输入: [10,2]
输出: "102"

示例 2:

输入: [3,30,34,5,9]
输出: "3033459"

答题

/**
 * @param {number[]} nums
 * @return {string}
 */
var minNumber = function(nums) { 
   return nums.sort((a,b) => `${b}${a}` - `${a}${b}`).join("")

};

3. 对比

1.类别

比较类

  • 交换类排序:快速排序、冒泡排序
  • 插入类排序:简单插入排序、希尔排序
  • 选择类排序:简单选择排序、堆排序
  • 归并排序:二路归并排序、多路归并排序

非比较类

  • 计数排序
  • 基数排序
  • 桶排序

对数组元素的要求

  • 计数排序、桶排序: 非负整数
  • 基数排序:整数

2.稳定性

稳定的排序算法

  • 冒泡排序
  • 选择排序
  • 插入排序
  • 归并排序
  • 桶排序
  • 基数排序

不稳定的排序算法

  • 快速排序
  • 希尔排序
  • 堆排序

3.时间复杂度

平方阶 (O(n^2)) 排序 各类简单排序:

  • 冒泡排序
  • 直接选择
  • 直接插入

线性对数阶 (O(nlog2n)) 排序

  • 快速排序
  • 堆排序
  • 归并排序

O(n1+§)) 排序,§ 是介于 0 和 1 之间的常数

  • 希尔排序

线性阶 (O(n)) 排序

  • 基数排序
  • 桶、箱排序

二、综合对比表

排序算法平均最差最好空间复杂度稳定性是否原地
冒泡排序O( n^2 )O( n^2 )O( n^2 )O(1)稳定原地
选择排序O( n^2 )O( n^2 )O( n^2 )O(1)稳定原地
插入排序O( n^2 )O( n^2 )O( n )O(1)稳定原地
希尔排序O(nlog2n)O( n^2 )O( nlog(n) )O(1)不稳定原地
归并排序O( nlog(n)O( nlog(n)O( nlog(n)O(n)稳定非原地
快速排序O( nlog(n) )O( n^2 )O( nlog(n) )O(1)不稳定原地
堆排序O( nlog(n)O( nlog(n)O( nlog(n)O(1)不稳定原地
计数排序O( n+k )O( n+k )O( n+k )O(n + k)稳定非原地
桶排序O( n+k )O( n^2 )O( n+k )O(n + k)稳定非原地
基数排序O( k * n )O( k * n )O( k * n )O(n + k)稳定非原地