算法心得——排序

简单排序

主要操作:比较两个数据项、交换两个数据项或复制其中一项,具体操作要看排序的类型。

1.冒泡排序

运行效率较低, 但是在概念上它是排序算法中最简单的,最适合初学的一种排序,具体思路为:

  • 对未排序的各元素从头到尾依次比较相邻的两个元素大小关系
  • 如果左边的队员高, 则两队员交换位置
  • 向右移动一个位置, 比较下面两个队员
  • 当走到最右端时, 最高的队员一定被放在了最右边
  • 继续从最左边开始排序,把第二高的队员排到倒数第二位置上
  • 依此类推,完成排序
ArrayList.prototype.bubbleSort = function () {
    // 1.获取数组的长度
    var length = this.array.length

    // 2.反向循环, 因此次数越来越少
    for (var i = length - 1; i >= 0; i--) {
        // 3.根据i的次数, 比较循环到i位置
        for (var j = 0; j < i; j++) {
            // 4.如果j位置比j+1位置的数据大, 那么就交换
            if (this.array[j] > this.array[j+1]) {
                // 交换
                this.swap(j, j+1)
            }
        }
    }
}

ArrayList.prototype.swap = function (m, n) {
    var temp = this.array[m]
    this.array[m] = this.array[n]
    this.array[n] = temp
}

冒泡排序的效率为O(N²),比较次数为N²,交换次数也为N²

2.选择排序

思路:

  • 选定第一个索引位置,然后和后面元素依次比较
  • 如果后面的队员, 小于第一个索引位置的队员, 则交换位置
  • 经过一轮的比较后, 可以确定第一个位置是最小的
  • 然后使用同样的方法把剩下的元素逐个比较即可
  • 可以看出选择排序,第一轮会选出最小值,第二轮会选出第二小的值,直到最后
ArrayList.prototype.selectionSort = function () {
    // 1.获取数组的长度
    var length = this.array.length

    // 2.外层循环: 从0位置开始取出数据, 直到length-2位置
    for (var i = 0; i < length - 1; i++) {
        // 3.内层循环: 从i+1位置开始, 和后面的内容比较
        var min = i
        for (var j = min + 1; j < length; j++) {
            // 4.如果i位置的数据大于j位置的数据, 那么记录最小的位置
            if (this.array[min] > this.array[j]) {
                min = j
            }
        }
        // 5.交换min和i位置的数据
        this.swap(min, i)
    }
}

选择排序虽然效率也是O(N²),但在一定程度上优化了冒泡排序的效率,比较次数跟选择排序的效率一样都是O(N²),交换次数是O(N)。

3.插入排序

插入排序是简单排序中效率最好的一种,也是学习其他高级排序的基础, 比如希尔排序/快速排序, 所以也非常重要。

思路:

  • 从第一个元素开始,该元素可以认为已经被排序
  • 取出下一个元素,在已经排序的元素序列中从后向前扫描
  • 如果该元素(已排序)大于新元素,将该元素移到下一位置
  • 重复上一个步骤,直到找到已排序的元素小于或者等于新元素的位置
  • 将新元素插入到该位置后, 重复上面的步骤
ArrayList.prototype.insertionSort = function () {
    // 1.获取数组的长度
    var length = this.array.length

    // 2.外层循环: 外层循环是从1位置开始, 依次遍历到最后
    for (var i = 1; i < length; i++) {
        // 3.记录选出的元素, 放在变量temp中
        var j = i
        var temp = this.array[i]

        // 4.内层循环: 内层循环不确定循环的次数, 最好使用while循环
        while (j > 0 && this.array[j-1] > temp) {
            this.array[j] = this.array[j-1]
            j--
        }

        // 5.将选出的j位置, 放入temp元素
        this.array[j] = temp
    }
}

插入排序对已经有序或基本有序的数据来说,效率高得多,它的效率还是O(N²),它的比较次数是选择排序的一半,所以这个算法效率是高于选择排序的。

高级排序

1. 希尔排序

希尔排序是插入排序的一种高效的改进版, 并且效率比插入排序要更快

插入排序的问题:当某个很小的数据比如1在最右端,要想把它放到正确的索引为0的位置,必须把每个前面的数据都向右移动一位,即比较和交换的次数都是N。

希尔排序就是在此基础上提出了一种不需要一个个移动所有中间数据项的算法,大致思路就是先将数据分成间隔比较大的分组进行排序,接着分成间隔小的分组进行排序,一步步缩小间隔,直到间隔为1。分完间隔排序后每次都让数据离自己正确位置更近了,间隔为1的排序就是插入排序了,此时大家都离自己正确的位置很近了,就不需要再对比交换那么多次数了,比如:

  • 有数组 81, 94, 11, 96, 12, 35, 17, 95, 28, 58, 41, 75, 15
  • 先让间隔为5, 进行排序. (35, 81), (94, 17), (11, 95), (96, 28), (12, 58), (35, 41), (17, 75), (95, 15)
  • 排序后的新序列, 一定可以让数字离自己的正确位置更近一步
  • 再让间隔位3, 进行排序. (35, 28, 75, 58, 95), (17, 12, 15, 81), (11, 41, 96, 94)
  • 排序后的新序列, 一定可以让数字离自己的正确位置又近了一步
  • 最后, 让间隔为1, 也就是正确的插入排序. 这个时候数字都离自己的位置更近, 那么需要复制的次数一定会减少很多
    在这里插入图片描述
    所以,希尔排序间隔的选取非常重要,大致的选择思路为:
  • 希尔排序的原稿中, 建议的初始间距是N / 2, 简单的把每趟排序分成两半
  • 希尔排序的效率很增量是有关系的,但是, 它的效率证明非常困难, 甚至某些增量的效率到目前依然没有被证明出来
  • 但是经过统计, 希尔排序使用原始增量, 最坏的情况下时间复杂度为O(N²), 通常情况下都要好于O(N²)
ArrayList.prototype.shellSort = function () {
    // 1.获取数组的长度
    var length = this.array.length

    // 2.根据长度计算增量
    var gap = Math.floor(length / 2)

    // 3.增量不断变量小, 大于0就继续排序
    while (gap > 0) {
        // 4.实现插入排序
        for (var i = gap; i < length; i++) {
            // 4.1.保存临时变量
            var j = i
            var temp = this.array[i]

            // 4.2.插入排序的内层循环
            while (j > gap - 1 && this.array[j - gap] > temp) {
                this.array[j] = this.array[j - gap]
                j -= gap
            }

            // 4.3.将选出的j位置设置为temp
            this.array[j] = temp
        }
      
        // 5.重新计算新的间隔
        gap = Math.floor(gap / 2)
    }
}

总之, 我们使用希尔排序大多数情况下效率都高于简单排序, 甚至在合适的增量和N的情况下, 还好好于快速排序

2.快速排序

快速排序几乎可以说是目前所有排序算法中, 最快的一种排序算法。当然希尔排序确实在某些情况下可能好于快速排序.,但是大多数情况下, 快速排序还是比较好的选择.
快速排序非常重要,是面试中提升水平的排序算法

快速排序是冒泡排序的升级版,冒泡排序要交换很多次才能将最大值放到正确的位置上,而快速排序可以在一次循环中(其实是递归调用)找出某个元素的正确位置, 并且该元素之后不需要任何移动。

快速排序最重要的思想是分而治之,比如:

  • 先选择其中一个数字,将大于数字的放到右边,小于数字的放到左边
  • 这个数字就被放到了自己肯定正确的地方
  • 接下来再对左右两堆数据进行递归
function quick_sort(arr) {
	let left = 0
	let right = arr.length-1
    if(left < right) {
		// 由x存储本轮要排序的数,默认每次都排第一个,要提高快排速度,最好的方法就是从左、中、右的中位数排起
        let x = arr[left];
        while (left < right)
        {
        	// 从最右边开始找第一个小于x的数
            while(arr[right] >= x) 
				right--;  
			// 找到了,判断是否在x右边
            if(left < right) 
            	// 把小于x的数放到x的位置上,同时左指针右移
				arr[left++] = arr[right];
			// 右边的数空出来了位置,从左向右找第一个大于等于x的数
            while(left < right && arr[left] < x) 
				left++;  
            if(left < right) 
            	// 把大于x的数放到空出来的位置上,同时右指针左移
				s[right--] = s[left];
        }
        // 找到了x最终的位置
        arr[left] = x;
        quick_sort(arr, 0, left - 1); // 递归调用 
        quick_sort(arr, left + 1, arr.length);
    }
    return arr
}

快速排序的最坏效率就是每次选择的枢纽都是最左边或最右边,此时效率等同于冒泡排序,但选择中位数的枢纽选择方法是不可能遇到最坏情况的,所以快速排序的平均效率是O(N * logN),其他某些算法的效率也可以达到O(N * logN), 但是快速排序是最好的。

3.归并排序

归并排序,是创建在归并操作上的一种有效的排序算法。算法采用分治法,且各层分治递归可以同时进行。归并排序思路简单,速度仅次于快速排序,为稳定排序算法,一般用于对总体无序,但是各子项相对有序的数列。

思路:

分解(Divide):将n个元素分成个含n/2个元素的子序列。
解决(Conquer):用合并排序法对两个子序列递归的排序。
合并(Combine):合并两个已排序的子序列已得到排序结果。

function mergeSort(arr) {
    const length = arr.length;
    if (length === 1) { //递归算法的停止条件,即为判断数组长度是否为1
        return arr;
    }
    const mid = Math.floor(length / 2);
   
    const left = arr.slice(0,  mid);
    const right = arr.slice(mid, length);
  
    return merge(mergeSort(left), mergeSort(right)); //要将原始数组分割直至只有一个元素时,才开始归并
}

function merge(left, right) {
    const result = [];
    let il = 0;
    let ir = 0;

    //left, right本身肯定都是从小到大排好序的
    while( il < left.length && ir < right.length) {
        if (left[il] < right[ir]) {
            result.push(left[il]);
            il++;
        } else {
            result.push(right[ir]);
            ir++;
        }
        
    }

    //不可能同时存在left和right都有剩余项的情况, 要么left要么right有剩余项, 把剩余项加进来即可
    while (il < left.length) { 
        result.push(left[il]);
        il++;
    }
    while(ir < right.length) {
        result.push(right[ir]);
        ir++;
    }
    return result;
}

4.堆排序

利用最大堆/最小堆的性质排序,基本思想是:将待排序序列构造成一个大顶堆,此时,整个序列的最大值就是堆顶的根节点。将其与末尾元素进行交换,此时末尾就为最大值。然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。如此反复执行,便能得到一个有序序列了。

function heapSort(arr) {
  let heapSize = arr.length;
  buildHeap(arr);//构造一个所有节点都满足arr[parent[i]] > arr[i]的堆结构数组,这样就把值最大的那个节点换到了根节点
  while(heapSize > 1) { //*1

    //在当前树中,交换位于根节点的最大值和最后一个节点的值,这样就把最大值排在了最后一个节点,这样就排好了最大值
    const temp = arr[0];
    arr[0]=arr[heapSize-1];
    arr[heapSize-1] = temp;

    heapSize--;//当前树中最后一个节点已经排好了值,故后面就不用再考虑这个节点,故新的树的大小减一

    if (heapSize>1) {
      heapify(arr, heapSize, 0);//上面的交换操作产生了新的根节点,新的根节点只是通过跟最后一个节点交换得到的值,故新的根节点不满足条件arr[parent[i]]<arr[i],所以要对根节点再次进行h
    }
  }
}

/**
 * @description 构造一个所有节点都满足arr[parent[i]] > arr[i]的堆结构数组
 * @param {Array} arr 待排序数组
 */
function buildHeap(arr) {
  const heapSize = arr.length;
  const firstHeapifyIndex = Math.floor(heapSize/2-1);//从树的倒数第二层的最后一个有子节点的节点(对于满二叉树就是倒数第二层的最后一个节点)开始进行heapify处理。Math.floor(heapSize/2-1)就是这个最后一个有子节点的节点索引。
  for (let i=firstHeapifyIndex; i >= 0; i--) {//从0到firstHeapifyIndex都要进行heapify处理,才能把最大的那个节点换到根节点
    heapify(arr, heapSize, i);
  }
}

/**
 * @description 以数组arr的前heapSize个节点为树,对其中索引为i的节点向子节点进行替换,直到满足从i往下的子节点都有arr[parent[i]]>=arr[i]
 * @param {*} arr TYPE Array  待排序的数组
 * @param {*} heapSize TYPE Number 待排序的数组中要作为当前树处理的从前往后数的节点个数,即待排序数组中前heapSize个点是要作为树来处理
 * @param {*} i TYPE Number arr数组中、heapSize长度的树中的当前要进行往子节点替换的节点的索引
 */
function heapify(arr, heapSize, i) {
  const leftIndex = i * 2 + 1;//索引i的节点的左子节点索引
  const rightIndex = i * 2 + 2;//索引i的节点的右子节点索引
  let biggestValueIndex = i;
  if (leftIndex < heapSize && arr[leftIndex] > arr[biggestValueIndex]) {
    //节点的最大index为heapSize-1
    //注意:这两次比较要跟arr[biggestValueIndex]比较,不能跟arr[i]比较,因为biggestValueIndex是会在左右i之间更新的
    biggestValueIndex = leftIndex; //如果左子节点的值大于biggestValueIndex的值(此时就是根节点的值),那么更新biggestValueIndex为左子节点索引
  }
  if (rightIndex < heapSize && arr[rightIndex] > arr[biggestValueIndex]) {
    biggestValueIndex = rightIndex;//如果右子节点的值大于biggestValueIndex的值(此时可能是根节点的值,也可能是左子节点的值),那么更新biggestValueIndex为右子节点索引
  }
  if (biggestValueIndex !== i) { //如果biggestValueIndex是左子节点索引或右子节点索引,那么交换根节点与biggestValueIndex节点的值
    const temp = arr[i];
    arr[i] = arr[biggestValueIndex];
    arr[biggestValueIndex] = temp;

    //交换后,被交换的那个子节点(左子节点或右子节点)往下可能就不再满足[parent[i]]>=arr[i],所以要继续对biggestValueIndex进行heaify处理,即将biggestValueIndex可能需要和子节点进行值交换,直到树的这个分支到叶子节点都满足arr[parent[i]]>=arr[i]
    heapify(arr, heapSize, biggestValueIndex);//要

  }
}

堆排序是一种选择排序,整体主要由构建初始堆+交换堆顶元素和末尾元素并重建堆部分组成。其中构建初始堆经推导复杂度为O(n),在交换并重建堆的过程中,需交换n-1次,而重建堆的过程中,根据完全二叉树的性质,[log2(n-1),log2(n-2)…1]逐步递减,近似为nlogn。所以堆排序时间复杂度一般认为就是O(nlogn)级。

5.计数排序

在这里插入图片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章