js 常见算法(一)

113 阅读6分钟

冒泡排序、选择排序、快速排序、插入排序思路及代码实现……


一 冒泡排序

冒泡排序的思路是,对所有相邻的值进行比较,如果符合条件,则将两者交换位置,最终达到有序化。

示例步骤:

  • 比较相邻的元素,如果前一个比后一个大(升序排列),交换位置。
  • 对每一对相邻的元素进行重复工作,每步完成后,最大元素出现在末尾。
  • 重复上两步,直到数组有序。

冒泡排序.gif

冒泡排序的基本思路:

// 一个乱序的数组
const arr = [8, 0, 4, 6, 1, 2, 7, 3, 5, 9];

// 冒泡排序方法
function bubbling (arr) {
  const len = arr.length;
  // 从头开始遍历数组
  for(let i = 0; i < len; i++) {
   	// 比较相邻元素
    for(let j = 0; j < len - 1; j++) {
      // 将数组按照升序排列,如果前一个元素大于后一个元素,则交换位置
      if (arr[j] > arr[j + 1]) {
        [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
      }
    }
  }
  return arr;
}

冒泡排序的优化:

// 一个乱序的数组
const arr = [8, 0, 4, 6, 1, 2, 7, 3, 5, 9];

// 冒泡排序方法
function bubbling (arr) {
  const len = arr.length;
  let flag = true; // 标志位
  // 标志位为 true 时执行循环,避免排好序之后继续循环
  for(let i = 0; i < len && flag; i++) {
    flag = false;
   	// j < len - 1 - i, 每次循环后,末位已排好的不再继续循环
    for(let j = 0; j < len - 1 - i; j++) {
      if (arr[j] > arr[j + 1]) {
        [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]];
        // 如果有交换的情况,说明数组不是有序的,继续循环
        flag = true;
      }
    }
  }
  return arr;
}

二 选择排序

选择排序的思路是,在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

示例步骤:

  • 在乱序数组中查找到最小元素(升序),存放到起始位置。
  • 在剩余元素内找到最小元素,存放到已排序元素的后面。
  • 重复上述步骤。

选择排序.gif

选择排序的基本思路:

// 乱序数组
const arr = [8, 0, 4, 6, 1, 2, 7, 3, 5, 9];

// 选择排序方法
function choose (arr) {
  // 最小元素的坐标
  let minIndex;
  const len = arr.length;
  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
      }
    }
    // 找到了最小坐标,替换到乱序数组头部
    [arr[i], arr[minIndex]] = [arr[minIndex], arr[i]];
  }
  return arr;
}

选择排序的优化:

/**
 * 双指针,同时处理最大值和最小值
 */
const arr = [8, 0, 4, 6, 1, 2, 7, 3, 5, 9];

function choose (arr) {
  // 左指针,从左到右开始遍历
  let left = 0;
  // 右指针,从右到左开始遍历
  let right = arr.length - 1;
  // 最大值
  let max;
  // 最小值
  let min;
  while (left < right) {
    max = left;
    min = left;
    for(let i = left + 1; i < arr.length - left; i++){
      // 同时寻找最小值和最大值坐标
      if(arr[i] > arr[max]){
        max = i;
      }
      if(arr[i] < arr[min]){
        min = i;
      }
    }
    // 如果找到的最大值不是末尾元素,则将最大值交换到末尾
    if(max !== right){
      swap(arr, max, right);
    }
    if(min === right){
      min = max;
    }
    // 如果找到的最小值不是头部元素,则将最小值交换到头部
    if(min !== left){
      swap(arr, min, left);
    }
    left ++;
    right --;
  }
  return arr;
}
function swap (arr, left, right) {
  [arr[left], arr[right]] = [arr[right], arr[left]]
}

三 快速排序

快速排序的思路是,从数列中挑出一个元素,以该元素为基准将数组分为两个部分,把小于基准值的数据集中到数组的左边(升序排列),把大于基准值的数据集中到数组的右边;数组的左边和右边重复上边的步骤,直到数组有序。

快速排序.gif

快速排序的基本思路:

/**
 * 该方法是用空间换取时间,每次细分都会创建左右两个数组,会加重内存的负担,对于配置不高的机器来说是不可取的
 */
// 快速排序函数
function quickSort(arr) {
  // 如果数组长度<=1,则说明数组不需要排序
  if (arr.length <= 1) {
    return arr;
  }
	// 找一个基准值索引,一般为数组中间的元素
  let middleIndex = Math.floor(arr.length / 2);

  // 获取基准值
  let middleValue = arr[middleIndex];
	
  // 建立一个左数组
  let left = [];

  // 建立一个右数组
  let right = [];

  // 遍历数组
  for (let i = 0,len = arr.length; i < len; i++){
    // 如果i为基准值索引,则跳过操作 
    if (i === middleIndex) {
      continue;
    }
    // 把小于基准值的数据放在左数组中
    if (arr[i] < middleValue) {
      left.push(arr[i]);
    } else {
      // 把大于基准值的数据放在右数组中
      right.push(arr[i]);
    }
  }
  // 将做数组、基准值和右数组整合在一块儿
  return quickSort(left).concat([middleValue], quickSort(right));
}

// quickSort([8, 0, 4, 6, 1, 2, 7, 3, 5, 9]); // [0,1,2,3,4,5,6,7,8,9] 得到有序数组

快速排序的优化:

/**
 * 使用左右两个指针来代替左右数组来对原数组排序
 * 时间换取空间
 */
function quick(array, left, right) {
  let index;
  if (array.length > 1) {
    index = sortOnce(array, left, right);
    if (left < index - 1) {
      // 对基准值左边的元素排序
      quick(array, left, index - 1);
    }
    if (index < right) {
      // 对基准值右边的元素排序
      quick(array, index, right);
    }
  }
  return array;
}
function sortOnce(arr, left, right) {
  const pivot = arr[Math.floor((right + left) / 2)];
  let i = left;
  let j = right;
  while (i <= j) {
    while (arr[i] < pivot) {
      i++;
    }
    while (arr[j] > pivot) {
      j--;
    }
    if (i <= j) {
      let middle = arr[i]
      arr[i] = arr[j]
      arr[j] = middle
      i++;
      j--;
    }
  }
  // 新增加,我们把处理之后的i值return出去
  return i
}

// 使用乱序数组验证
const arr = [8, 0, 4, 6, 1, 2, 7, 3, 5, 9];
quick(arr, 0, arr.length - 1) // [ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 ] 得到有序数组

四 插入排序

插入排序思路是,在要排序的一组数中,假定前 n-1 个数已经排好序,现在将第 n 个数插到前面的有序数列中,使得这第 n 个数也是排好顺序的。如此反复循环,直到全部排好顺序。

示例步骤:

  • 获得一个待排序的值 – 目标值。
  • 从后向前遍历此元素之前的元素,如果元素大于目标值,将元素后移一个单位。
  • 元素小于或等于目标值,将目标值放在此元素之后。

插入排序.gif

直接插入排序:

// 待排序数组
const arr = [8, 0, 4, 6, 1, 2, 7, 3, 5, 9];

// 直接插入排序
function insertSort(arr) {
  // 传入的数组长度
  const len = arr.length;
  // 由于数组的第一个元素不需要排序,所以i从1开始
  for (let i = 1; i < len; i++) {
    // 获取目标值
    let result = arr[i];
    // 待插入的坐标
    let resultIndex;
    // 从i向前遍历,如果大于目标值,则后移一位
    for (resultIndex = i; resultIndex > 0 && (arr[resultIndex - 1] > result); resultIndex--) {
      arr[resultIndex] = arr[resultIndex - 1];
    }
    // 将目标值放入适当的坐标中
    arr[resultIndex] = result;
  }
}

折半插入排序:

/**
 * 折半插入是在直接插入的过程中使用 二分查找(会在查找方法中详细介绍) 方法搜索坐标
 */
function binaryInsertSort(arr) {
  let start; // 二分查找的起始点
  let end; // 二分查找的终止点
  let resultIndex; // 插入坐标
  let result; // 目标值
  for (let i = 1; i < arr.length; i++) {
    if (arr[i] < arr[i - 1]) {
      // 获取目标值
      result = arr[i];
      // 获取起始坐标
      start = 0;
      // 获取终止坐标
      end = i - 1;
      // 在此区间之内进行二分查找
      while (start <= end) {
        // 中间值索引
        let mid = Math.floor((start + end) / 2);
        // 目标值 > 中间值
        if (result > arr[mid]) {
          start = mid + 1;
        } else {
          // 目标值 < 中间值
          end = mid - 1;
        }
      }
      // 将start 到 resultIndex之间的元素统一后移一位
      for (resultIndex = i; resultIndex > start; resultIndex--) {
        arr[resultIndex] = arr[resultIndex - 1];
      }
      // 将目标值放入插入坐标
      arr[resultIndex] = result;
    }
  }
}

参考:JAVA资讯库