数据结构和算法四(排序)

183 阅读2分钟

快速排序

快速排序:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据比另一部分的所有数据要小,再按这种方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,使整个数据变成有序序列。 快速排序也利用了分治的思想(将问题分解成一些小问题递归求解

思路

  • 选择一个基准元素target(一般选择第一个数)
  • 将比target小的元素移动到数组左边,比target大的元素移动到数组右边
  • 分别对target左侧和右侧的元素进行快速排序

下面是对序列6、1、2、7、9、3、4、5、10、8排序的过程:

解法1

  • 单独开辟两个存储空间left和right来存储每次递归比target小和大的序列
  • 每次递归直接返回left、target、right拼接后的数组
function quickSort(array) {
  if (array.length < 2) {
    return array;
  }
  const target = array[0];
  const left = [];
  const right = [];
  for (let i = 1; i < array.length; i++) {
    if (array[i] < target) {
      left.push(array[i]);
    } else {
      right.push(array[i]);
    }
  }
  return quickSort(left).concat([target], quickSort(right));
}

解法2

  • 记录一个索引l从数组最左侧开始,记录一个索引r从数组右侧开始
  • l<r的条件下,找到右侧小于target的值array[r],并将其赋值到array[l]
  • l<r的条件下,找到左侧大于target的值array[l],并将其赋值到array[r]
  • 这样让l=r时,左侧的值全部小于target,右侧的值全部小于target,将target放到该位置
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);
    return arr;
}

复杂度

时间复杂度:平均O(nlogn),最坏O(n2),实际上大多数情况下小于O(nlogn)

空间复杂度:O(logn)(递归调用消耗)

归并排序

利用归并的思想实现的排序方法。 该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。(分治法将问题分成一些小的问题然后递归求解,而治的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。

思路:

  • 将已有序的子序列合并,得到完全有序的序列
  • 即先使每个子序列有序,再使子序列段间有序
  • 若将两个有序表合并成一个有序表,称为二路归并

分割:

  • 将数组从中点进行分割,分为左、右两个数组
  • 递归分割左、右数组,直到数组长度小于2

归并:

  • 如果需要合并,那么左右两数组已经有序了。
  • 创建一个临时存储数组temp,比较两数组第一个元素,将较小的元素加入临时数组
  • 若左右数组有一个为空,那么此时另一个数组一定大于temp中的所有元素,直接将其所有元素加入temp

解法1

分割数组时直接将数组分割为两个数组,合并时直接合并数组。

function mergeSort(array) {
  if (array.length < 2) {
    return array;
  }
  const mid = Math.floor(array.length / 2);
  const front = array.slice(0, mid);
  const end = array.slice(mid);
  return merge(mergeSort(front), mergeSort(end));
}

function merge(front, end) {
  const temp = [];
  while (front.length && end.length) {
    if (front[0] < end[0]) {
      temp.push(front.shift());
    } else {
      temp.push(end.shift());
    }
  }
  while (front.length) {
    temp.push(front.shift());
  }
  while (end.length) {
    temp.push(end.shift());
  }
  return temp;
}

解法2

记录数组的索引,使用left、right两个索引来限定当前分割的数组。

function mergeSort(array, left, right, temp) {
  if (left < right) {
    const mid = Math.floor((left + right) / 2);
    mergeSort(array, left, mid, temp)
    mergeSort(array, mid + 1, right, temp)
    merge(array, left, right, temp);
  }
  return array;
}

function merge(array, left, right, temp) {
  const mid = Math.floor((left + right) / 2);
  let leftIndex = left;
  let rightIndex = mid + 1;
  let tempIndex = 0;
  while (leftIndex <= mid && rightIndex <= right) {
    if (array[leftIndex] < array[rightIndex]) {
      temp[tempIndex++] = array[leftIndex++]
    } else {
      temp[tempIndex++] = array[rightIndex++]
    }
  }
  while (leftIndex <= mid) {
    temp[tempIndex++] = array[leftIndex++]
  }
  while (rightIndex <= right) {
    temp[tempIndex++] = array[rightIndex++]
  }
  tempIndex = 0;
  for (let i = left; i <= right; i++) {
    array[i] = temp[tempIndex++];
  }
}

复杂度

时间复杂度:O(nlogn) 空间复杂度:O(n)

选择排序

每次循环选取一个最小的数字放到前面的有序序列中。

function selectionSort(array) {
  for (let i = 0; i < array.length - 1; i++) {
    let minIndex = i;
    for (let j = i + 1; j < array.length; j++) {
      if (array[j] < array[minIndex]) {
        minIndex = j;
      }
    }
    [array[minIndex], array[i]] = [array[i], array[minIndex]];
  }
}

复杂度

时间复杂度:O(n2)

空间复杂度:O(1)

插入排序

将左侧序列看成一个有序序列,每次将一个数字插入该有序序列。

插入时,从有序序列最右侧开始比较,若比较的数较大,后移一位。

function insertSort(array) {
  for (let i = 1; i < array.length; i++) {
    let target = i;
    for (let j = i - 1; j >= 0; j--) {
      if (array[target] < array[j]) {
        [array[target], array[j]] = [array[j], array[target]]
        target = j;
      } else {
        break;
      }
    }
  }
  return array;
}

复杂度

时间复杂度:O(n2) 空间复杂度:O(1)

冒泡排序

循环数组,比较当前元素和下一个元素,如果当前元素比下一个元素大,向上冒泡。 这样一次循环之后最后一个数就是本数组最大的数。 下一次循环继续上面的操作,不循环已经排序好的数。 优化:当一次循环没有发生冒泡,说明已经排序完成,停止循环。

function bubbleSort(array) {
  for (let j = 0; j < array.length; j++) {
    let complete = true;
    for (let i = 0; i < array.length - 1 - j; i++) {
      // 比较相邻数
      if (array[i] > array[i + 1]) {
        [array[i], array[i + 1]] = [array[i + 1], array[i]];
        complete = false;
      }
    }
    // 没有冒泡结束循环
    if (complete) {
      break;
    }
  }
  return array;
}

复杂度

时间复杂度:O(n2) 空间复杂度:O(1)

堆排序

创建一个大顶堆,大顶堆的堆顶一定是最大的元素。

交换第一个元素和最后一个元素,让剩余的元素继续调整为大顶堆。

从后往前以此和第一个元素交换并重新构建,排序完成。

function heapSort(array) {
  creatHeap(array);
  console.log(array);
  // 交换第一个和最后一个元素,然后重新调整大顶堆
  for (let i = array.length - 1; i > 0; i--) {
    [array[i], array[0]] = [array[0], array[i]];
    adjust(array, 0, i);
  }
  return array;
}
// 构建大顶堆,从第一个非叶子节点开始,进行下沉操作
function creatHeap(array) {
  const len = array.length;
  const start = parseInt(len / 2) - 1;
  for (let i = start; i >= 0; i--) {
    adjust(array, i, len);
  }
}
// 将第target个元素进行下沉,孩子节点有比他大的就下沉
function adjust(array, target, len) {
  for (let i = 2 * target + 1; i < len; i = 2 * i + 1) {
    // 找到孩子节点中最大的
    if (i + 1 < len && array[i + 1] > array[i]) {
      i = i + 1;
    }
    // 下沉
    if (array[i] > array[target]) {
      [array[i], array[target]] = [array[target], array[i]]
      target = i;
    } else {
      break;
    }
  }
}

复杂度

时间复杂度:O(nlogn) 空间复杂度:O(1)