经典排序算法(附JavaScript实现,选择,插入,冒泡,快排,归并,后续补充中)

470 阅读4分钟

排序怎么™这么多种??为啥要学算法??除了一堆高大上编出来的理由,可能最现实的就是。。。前端太™卷了,大厂都面试算法,你不得不跟着卷。

选择排序

选择排序就是将数组分成有序和无序数组,将无序数组首位作为最小值,遍历无序数组,与最小值比较,若比当前设置最小值小,将其作为最小值,循环结束后将最小值与无序数组首位交换,有序数组长度+1,无序数组长度-1,循环往复,直到无序数组长度为0。核心就是从无序数组中选择一个最小值

如: 数组[1,3,4,8,2,5,7],我们按从小到大排序,后续例子都如此

[]作为有序数组, [1,3,4,8,2,5,7]作为无序数组

第一步:循环无序数组取出最小值为1,放置于有序数组末尾,无序数组长度减一,有序数组为[1],无序数组为[3,4,8,2,5,7]

第二步:循环无序数组取出最小值为2,同上,有序数组为[1,2],无序数组为[3,4,8,5,7]

...

循环N次后数组排序完成,代码如下:

function selectSort(arr) {
    const len = arr.length;
      if (len <=1 ) return arr
      let minIndex, temp;
      // 遍历N次
      for (let i=0;i<len-1;i++) {
        // 遍历无序数组,将数组第一位作为最小值,后续与其比较取出最小值下标
        minIndex = i;
        for (let j=i+1;j<len;j++) {
          if (arr[j] < arr[minIndex]) {
            minIndex = j;  
          }
        }
        temp = arr[i]
        arr[i] = arr[minIndex];
        arr[minIndex] = temp;
      }
    return arr
}

该算法的时间复杂度为O(n²),空间复杂度为O(1)。因为选择排序需要在无序数组中做交换操作,所以他是不稳定排序

插入排序

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

如:数组[1,3,4,8,2,5,7]

初始,我们将[1]作为有序数组,[3,4,8,2,5,7]作为无序数组

第一步:将无序数组第一位3插入有序数组最后,有序数组变为[1,3],无序数组为[4,8,2,5,7]

第二步:将无序数组第一位4插入有序数组,有序数组为[1,3,4], 无序数组为[8,2,5,7]

第三步:同上,有序数组为[1,3,4,8],无序数组为[2,5,7]

第四步:无序数组首位为2,这时需要找到有序数组中合适位置,从有序数组最后一位开始遍历,若该值大于2,将其后移一位,取有序数组倒数第二位比较,循环往复,直到有序数组中有值小于2,因为大于2的数都往后移动了一位,将2直接放置于当前位置即可,即:有序数组为[1,2,3,4,8],无序数组为[5,7]

...

代码如下:

function insertSort(arr) {
  const len = arr.length;
  if (len <= 1) return arr;
  for (let i=1;i<len;i++) {
   const value = arr[i];
   let j=i-1;
   for (;j>=0;j--) {
     if (arr[j] > value) {
       arr[j+1] = arr[j]
     } else {
       break;
     }     
   }
   arr[j+1] = value;
  }  
  return arr;
}

该算法的时间复杂度为O(n²),空间复杂度为O(1),因为他是将无序数组往有序数组后依次插入,不会打乱无序数组同等值本来的先后关系,所以是稳定排序

冒泡排序

冒泡算法,核心当然是冒泡了,怎么冒泡呢,我们把数组从首位开始,让相邻两位数据进行比较,根据大小关系交换或者不交换后,往后继续,直到所有两两相邻的数据交换完成,这时最大(或最小)的数已经到数组最尾部,就如从数组中某个位置冒泡到最尾部。我们做N遍(N=数组长度),数组就排序完成了。

如:数组[1,3,4,8,2,5,7]

第一次循环:13比较,不交换,34比较,不交换,如此下去,直到82比较,两者交换,数组变为[1,3,4,2,8,5,7],再将85交换,变为[1,3,4,2,5,8,7],再将87交换,得到第一次循环的结果[1,3,4,2,5,7,8]

...

如此循环N次,代码如下:

function bubbleSort(arr) {
  const len = arr.length;
  if (len <= 1) return arr;
  for (let i = 0;i < len - 1;i++) {
    // 标志这次循环是否有元素交换,若没有,则不需要再继续循环
    let flag = false;
    // 需要冒泡的长度随着循环值变大而缩小
    for (let j = 0; j < len - 1- i;j++) {
      if (arr[j] > arr[j+1]) {
        const temp = arr[j]
        arr[j] = arr[j+1]
        arr[j+1] = temp
        flag = true;
      }
    }
    if (!flag) break;
  }
  return arr
}

该算法的时间复杂度为O(n²),空间复杂度为O(1),因为冒泡排序是交换排序,如果两个值大小相等就不需要交换,所以是稳定排序

快速排序

快速排序利用的是分治的思想,将一个无序数组随机取一个值作为分区点(pivot),将小于pivot的值放于数组左侧,大于pivot的值放于数组右边。然后利用递归的思想,将左右数组继续排序,直到分区的数组长度为1,这样这个数组就排序好了。

例如数组[1,3,4,8,2,5,7],我们取数组中间值作为pivot

左侧小于pivot的数组为[1,3,4,2,5,7],中间为[8], 右侧为[]

继续对左侧进行分区排序,取4为pivot, 分为[1,3,2], [4][5, 7],继续下去

递归公式为:quick_sort(l,r) = quick_sort(l, pivot - 1) + [pivot] + quick_sort(pivot + 1, r)

终止条件为:l >= r

代码如下:

funciton quickSort(arr) {
   const len = arr.length;
    if (len <= 1) return arr;
    // 取中间点作为pivot,你也可以取首置位,末置位的值,没有硬性条件
    const pivotIndex = Math.floor(len/2);
    // 从数组中删除pivot,此时数组长度-1
    const pivot = arr.splice(pivotIndex, 1)[0];
    const left = [], right = [];
    for (let i = 0;i < len - 1;i++) {
      if (arr[i] < pivot) {
        left.push(arr[i])
      } else {
        right.push(arr[i])
      }
    }
    return sortArray(left).concat([pivot], sortArray(right))
}

这个算法有个问题是空间复杂度较高,每次分区都会新建数组。我们可以通过原地排序将空间复杂度降低为O(1)。 如果要原地排序,我们使用类似选择排序的方法去分界左右两边数组,使得左边的数组小于pivot, 右边数组大于pivot, 需要将数组传递下去,优化后:

    function quickSort(arr) {
      const len = arr.length;
      if (len <= 1) return arr;
      quick_sort(arr, 0, len -1)
    }
    
    function quick_sort(A, l, r) {
        if (l >= r) return;
        // 取最后一个数作为pivot
        const pivot = A[r];
        let i=l,j=l;
        // 将数组分为有序和无序两个数组,i作为有序和无序数组的分界游标,使用j遍历无序数组
        while (j < r) {
          if (A[j] < pivot) {
            if (i !== j) {
              const temp = A[j];
              A[j] = A[i];
              A[i] = temp;
            }           
            // 有序数组长度加1,分界游标i+1
            i++;
          }
          j++;
        }
        // 因为只遍历到了j = povit下标 - 1,需要将pivot与分界游标的值替换,使得i的值为真正的分界值
        if (i !== r) {
          A[r] = A[i];
          A[i] = pivot;
        }
               
        quick_sort(A, l, i-1);
        quick_sort(A, i+1, r);
    }

该算法的时间复杂度是O(nlogn),空间复杂度是O(1),因为需要做类似选择排序的交换操作,如数组[6,7,6,8,3,4,5],以5作为pivot,当j=4时,需要将A[i]与A[j]交换,此时两个6顺序颠倒,所以快速排序是不稳定排序

归并排序

归并排序也是一种分治的思想,将一个数组从中间分成两个数组,然后继续往下分,直到分无可分(即左右各只有长度为1的数组),然后左右两边排序,再将数组合并,一直往上,直到成为有序数组。如图所示:

stateDiagram-v2
[3,4,7,8,6,5] --> [3,4,7]
[3,4,7,8,6,5] --> [8,6,5]
[3,4,7] --> [3]
[3,4,7] --> [4,7]
[8,6,5] --> [8]
[8,6,5] --> [6,5]
[4,7] --> [4]
[4,7] --> [7]
[4] --> [4,7]*
[7] --> [4,7]*
[3] --> [3,4,7]*
[4,7]* --> [3,4,7]*
[6,5] --> [6]
[6,5] --> [5]
[6] --> [5,6]*
[5] --> [5,6]*
[8] --> [5,6,8]*
[5,6]* --> [5,6,8]*
[3,4,7]* --> [3,4,5,6,7,8]*
[5,6,8]* --> [3,4,5,6,7,8]*


归并算法的递推公式为: merge_sort(l,r) = merge_sort(l, mid) + mergeSort(mid+1, r)

终止条件为: l >= r 表示不需要继续拆分

function mergeSort(arr) {
    return merge_sort(arr)
}

function merge_sort(arr) {
  const len = arr.length;
  if (len <= 1) return arr;
  const mid = Math.floor(len/2);
  const left = merge_sort(arr.splice(0, mid));
  const right = merge_sort(arr);
  return merge_sort_f(left, right);
}

function merge_sort_f(left, right) {
  // 数组后增加哨兵,减少越界判断
  left.push(Infinity)
  right.push(Infinity)
  const temp = [];
  let i= 0, j = 0;
  while(left[i] < Infinity || right[j] < Infinity) {
    if (left[i] <= right[j]) {
      temp.push(left[i])
      i++;
    } else {
      temp.push(right[j])
      j++;
    }    
  }
  return temp;
}

该算法的时间复杂度为O(nlogn),空间复杂度为O(n),归并算法是稳定排序


有更好的优化方案麻烦大家提出来,我再改进~

另外不会画图,我看别人都是图文结合,很蛋疼,不知道哪位大佬提供个好办法~

欢迎大家在评论区提出意见和建议!