数据结构之排序算法

140 阅读4分钟

介绍

考量排序算法优劣的三个标准:

执行效率, 内存消耗, 稳定性

冒泡、插入、选择排序都有一个共同点,那就是将待排序数列分为已排序和未排序两部分。在未排序的部分中查找一个最值,放到已排序数列的恰当位置

具体到代码层面,外层循环的变量用于分割已排序和未排序数,内层循环的变量用于在未排序数中查找。从思路上看,这三种算法其实是一样的,所以时间复杂度也相同。

img

希尔排序

function shellSort (arr) {
  let len = arr.length
  // gap 即为增量
  for (let gap = Math.floor(len / 2); gap > 0; gap = Math.floor(gap / 2)) {
    for (let i = gap; i < len; i++) {
      let j = i
      let current = arr[i]
      while (j - gap >= 0 && current < arr[j - gap]) {
        arr[j] = arr[j - gap]
        j = j - gap
      }
      arr[j] = current
    }
  }
  return arr
}


var arr = [3, 5, 7, 1, 4, 56, 12, 78, 25, 0, 9, 8, 42, 37]
console.log(shellSort(arr))

冒泡排序

  1. 排序中交换的次数 = 逆序度 ;

    逆序度 = 满有序度 - 有序度,满有序度为n*(n-1)/2

  2. 空间复杂度为O(1),是原地排序算法;平均时间复杂度为O(n²)。

    平均情况下,需要 n (n-1)/4 次交换操作,比较操作肯定要比交换操作多,而复杂度的上限是 O(n2),所以平均情况下的时间复杂度就是 O(n2)。

  3. 是稳定的排序算法

  4. 排序的过程是增加有序度(从小到大),减少逆序度,最后达到满有序度

// 冒泡排序,a表示数组,n表示数组大小
public void bubbleSort(int[] a, int n) {
  if (n <= 1) return; 
 for (int i = 0; i < n; ++i) {
    // 提前退出冒泡循环的标志位
    boolean flag = false;
    //此处的n-i-1是关键,要结合理解:每次遍历的下标范围从0~n-1->0~n-2->……->0~1
    for (int j = 0; j < n - i - 1; ++j) {
      if (a[j] > a[j+1]) { // 交换
        int tmp = a[j];
        a[j] = a[j+1];
        a[j+1] = tmp;
        flag = true;  // 表示有数据交换      
      }
    }
    if (!flag) break;  // 没有数据交换,提前退出
  }
}

插入排序

【小白学算法】排序专题之“插入排序”,JavaScript实现,小白经典必学系列!_哔哩哔哩_bilibili

  1. 空间复杂度为O(1),是原地排序算法

  2. 是稳定排序算法

  3. 时间复杂度

    1. 最好O(n)
    2. 最坏=平均O(n2)
// 插入排序,a表示数组,n表示数组大小
public void insertionSort(int[] a, int n) {
  if (n <= 1) return;
​
  for (int i = 1; i < n; ++i) {
    int value = a[i];
    int j = i - 1;
    // 查找插入的位置
    for (; j >= 0; --j) {
      if (a[j] > value) {
        a[j+1] = a[j];  // 数据移动
      } else {
        break;
      }
    }
    a[j+1] = value; // 插入数据
  }
}

1. 概念

打过扑克牌的人应该秒懂, 插入排序是一种最简单直观的排序算法, 它的工作原理是通过构建有序序列, 对于未排序数据, 在已排序序列中从后向前扫描, 找到相应位置并插入

2. 思路

  1. 将第一待排序序列第一个元素看作一个有序序列, 把第二个元素到最后一个元素当成是未排序序列
  2. 从头到尾依次扫描未排序序列, 将扫描到的每个元素插入有序序列的适当位置

image.png

3. 代码实现

const arr = readline().split(',').map(Number)

function insertSort(arr) {
    let len = arr.length
    let preIndex //指向
    for (let i=1; i<len; i++) {
        preIndex = i-1
        
    }
}

冒泡和插入的比较

//冒泡排序中数据的交换操作:
if (a[j] > a[j+1]) { // 交换
   int tmp = a[j];
   a[j] = a[j+1];
   a[j+1] = tmp;
   flag = true;
}
​
//插入排序中数据的移动操作:
if (a[j] > value) {
  a[j+1] = a[j];  // 数据移动
} else {
  break;
}

选择排序

  1. 空间复杂度为O(1),是原地排序算法
  2. 是不稳定的排序算法
  3. 时间复杂度:最好=最坏=平均O(n2)

1. 概念

典型的时间换空间

选择排序是一种简单直观的排序算法, 无论什么数据进去都是O(n^2)的时间复杂度

所以用到它的时候, 数据规模越小越好, 唯一的好处就是不占用额外的存储空间

2. 思路

首先在未排序序列中找到最小(大)元素, 存放到排序序列的起始位置

再从剩余未排序元素中继续寻找最小(大)元素, 然后放到已排序序列的末尾

重复第二步, 直到所有元素排序完毕

image.png

image.png

3. 代码实现

// 选择排序
function selectionSort(arr){
    // 保存最小值索引
    let minIndex, temp;
    // 外循环: 获取首个元素(直到倒数第二个元素)
    for (let i=0; i<arr.length-1; i++){
        // 假设最小值是i, 也就是未排序序列的排头元素
        minIndex = i;
        // 从i的后面位置找最小值
        // 内循环(直到最后一个元素)
        for (let j=i+1; j<arr.length; j++){
            if (arr[j]<arr[minIndex]) {
                minIndex = j;
            }
        }
        // 交换排头元素与最小值
        temp = arr[i];
        arr[i] = arr[minIndex];
        arr[minIndex] = temp;
    }
    return arr;
}
// 输入处理, 将传入的字符串转换为一个数组
const arr = readline().split(',');
// 打印验证
console.log(selectionSort(arr).join(',');

归并排序

  • 是一种稳定排序算法
  • 时间复杂度在任何情况下都是O(nlogn)
  • 空间复杂度为O(n), 不是原地排序算法
// 合并两个有序数组(在两个数组上归并)
function merge_two_sortedArr (arr1, arr2) {
  const l1 = arr1.length
  const l2 = arr2.length
  let p = 0
  let q = 0
  let res = []
  while (p < l1 || q < l2) {
    if (p === l1) {
      res.push(arr2[q++])
    } else if (q === l2) {
      res.push(arr1[p++])
    } else if (arr1[p] < arr2[q]) {
      res.push(arr1[p++])
    } else {
      res.push(arr2[q++])
    }
  }
  return res
}
let arr1 = [1, 4, 6]
let arr2 = [3, 12, 88]
console.log(merge_two_sortedArr(arr1, arr2))
// 在原数组上进行归并
function merge_in_place (arr, l, mid, r) {
  let keep = []
  for (let i = l; i <= r; i++) {
    keep[i] = arr[i]
  }
  let p = l
  let q = mid + 1
  for (let i = l; p <= mid || q <= r; i++) {
    if (p === mid + 1) {
      arr[i] = keep[q++]
    } else if (q === r + 1) {
      arr[i] = keep[p++]
    } else if (keep[p] < keep[q]) {
      arr[i] = keep[p++]
    } else {
      arr[i] = keep[q++]
    }
  }
  return keep
}
let arr = [1, 4, 5, 2, 3, 6, 7]
merge_in_place(arr, 0, 2, arr.length - 1)
console.log(arr)
// 归并排序
function merge (arr, l, mid, r) {
  const keep = []
  for (let i = l; i <= r; i++) {
    keep[i] = arr[i]
  }

  for (let p = l, q = mid + 1, k = l; p <= mid || q <= r;) {
    if (p === mid + 1) arr[k++] = keep[q++]
    else if (q === r + 1) arr[k++] = keep[p++]
    else if (keep[p] > keep[q]) arr[k++] = keep[q++]
    else arr[k++] = keep[p++]
  }
}

function mSort (arr, l, r) {
  if (l >= r) return
  // 分半
  const mid = l + Math.floor((r - l) / 2)
  // 归并排序左侧
  mSort(arr, l, mid)
  // 归并排序右侧
  mSort(arr, mid + 1, r)
  // 走到这里, l~mid 部分已排序,mid+1~r 部分已排序
  // 直接原地归并操作就可以实现数组排序
  merge(arr, l, mid, r)
}

function MergeSort (arr) {
  mSort(arr, 0, arr.length - 1)
}

const nums = [12, 1, 7, 4, 5, 2, 10, 6, 3, 11, 9, 8, 13]
MergeSort(nums)
console.log(nums);

快速排序

快排是一种原地、不稳定的排序算法, 运用了分治的思想

时间复杂度:

  • 最优时间复杂度(分区极其均衡)
    • O(nlogn)
  • 最坏时间复杂度(分区极其不均衡, 例如对有序数组进行排序)
    • O(n^2)
  • 平均时间复杂度
    • O(n)

在大部分情况下的时间复杂度都可以做到 O(nlogn),只有在极端情况下,才会退化到 O(n2),同时我们也有很多方法将这个概率降到极低

算法:

  • 第一步:选择数组的开头元素作为基数 empty = arr[begin]
  • 第二步:将序列中大于基数的放在基数右边,小于基数的放在基数的左边
    • 双while循环
  • 第三步:对基数的左边和右边两个序列重复第二步和第三步
    • quickSort(arr, begin, i - 1)
    • quickSort(arr, i + 1, end)

代码:

function quickSort (arr, begin, end) {
  if (begin >= end) return
  let i = begin,
    j = end,
    empty = arr[begin]
  // 第二步
  while (i < j) {
    // 先移动j
    while (i < j && arr[j] > empty) {
      j--
    }
    arr[i] = arr[j]
    // 再移动i
    while (i < j && arr[i] < empty) {
      i++
    }
    arr[j] = arr[i]
  }
  // 此时i,j在同一位置, i==j
  arr[i] = empty
  // 第三步
  quickSort(arr, begin, i - 1)
  quickSort(arr, i + 1, end)
}

let arr = [2, 5, 3, 7, 6, 4, 21, 13, 1]
quickSort(arr, 0, arr.length - 1)
console.log(arr)