我常用的排序算法

学习排序算法的目的

  1. 解决实际问题:排序算法是计算机科学中的基础算法之一,几乎在任何领域的软件开发中都会遇到排序的需求。了解各种排序算法能够帮助程序员解决实际问题,提供高效的数据处理和查询功能。
  2. 性能优化:排序算法的选择会对程序的性能产生直接影响。不同的排序算法在时间复杂度和空间复杂度上有不同的特点,了解不同排序算法的性能特点可以帮助程序员选择最适合的算法,优化程序的执行效率。
  3. 数据结构:排序算法与数据结构密切相关。对于不同类型的数据结构,选择合适的排序算法能够充分发挥数据结构的优势,提高数据处理的效率。例如,对于链表结构,插入排序可能更加适用,而对于数组结构,快速排序或堆排序可能更高效。
  4. 算法设计和分析:排序算法是算法设计和分析的经典案例之一。通过学习排序算法,程序员可以培养良好的算法设计思维,学会分析和评估算法的性能,提高解决问题的能力。
  5. 面试准备:排序算法是面试中常见的考点之一。许多技术面试会涉及排序算法的实现和性能分析,熟悉排序算法可以帮助程序员在面试中更好地展示自己的技术能力。

常见的十大排序算法分类

  1. 比较类排序:通过比较来决定元素间的相对次序,由于其时间复杂度不能突破O(nlogn),因此也称为非线性时间比较类排序。
  2. 非比较类排序:不通过比较来决定元素间的相对次序,它可以突破基于比较排序的时间下界,以线性时间运行,因此也称为线性时间非比较类排序。

冒泡排序(Bubble Sort)

重复地遍历要排序的列表,依次比较相邻的两个元素,并按照升序或降序交换它们的位置,直到整个列表排序完成。

实现原理

  1. 从列表的第一个元素开始,将其与下一个元素比较,如果顺序错误(升序时当前元素大于下一个元素,降序时当前元素小于下一个元素),则交换它们的位置。
  2. 继续比较下一个相邻元素,重复上述步骤,直到遍历到列表的最后一个元素。
  3. 重复步骤1和2,直到没有任何元素需要交换位置,即列表已排序完成。
冒泡.gif
function bubbleSort(arr) {
    var len = arr.length;
    for (var i = 0; i < len - 1; i++) {
        for (var j = 0; j < len - 1 - i; j++) {
            if (arr[j] > arr[j+1]) {        // 相邻元素两两对比
                var temp = arr[j+1];        // 元素交换
                arr[j+1] = arr[j];
                arr[j] = temp;
            }
        }
    }
    return arr;
}

使用场景

冒泡排序是一种简单但效率较低的排序算法,适用于小规模的列表;在大规模数据的情况下,其他高效的排序算法(如快速排序、归并排序)更常被使用。
列表几乎已经有序时,冒泡排序可能会比其他算法更加高效。

快速排序(Quick Sort)

快速排序(Quick Sort)是一种高效的排序算法,它的原理是通过选择一个基准元素,将数组分割成较小和较大两个子数组,然后对子数组进行递归排序,直到整个数组有序。

实现原理

  1. 选择一个基准元素(通常是数组中的中间元素)。
  2. 将数组分成两个子数组,比基准元素小的放在左边,比基准元素大的放在右边。
  3. 递归地对左右子数组进行快速排序。
  4. 合并左子数组、基准元素和右子数组,得到最终有序的数组。
  function quickSort(arr) {
    // 检查数组元素的个数,如果小于等于1,就返回
    if (arr.length <= 1) return arr;
    // 基准索引
    let pivotIndex = Math.floor(arr.length / 2);
    // 找到基准,并且把基准从原数组中删除
    let pivot = arr.splice(pivotIndex, 1)[0];
    // 定义左右数组
    let left = [];
    let right = [];
    // 把基准小的放在left数组中,大的放在right中
    arr.forEach(ele => {
      if (ele < pivot) {
        left.push(ele);
      } else {
        right.push(ele);
      }
    });
    return quickSort(left).concat([pivot], quickSort(right));
  }
console.log("快速排序",quickSort([49, 38, 65, 97, 76, 13, 27, 49]));

使用场景

适用于大规模数据的排序,它在实际应用中被广泛使用
许多编程语言和库中的默认排序算法,如JavaScript中的Array.prototype.sort()方法就使用了快速排序算法。

选择排序(Selection Sort)

在一组数据中,首先找到最小(或最大)的数据并放在最前面(数据的后面),以此类推,直到全部数据排序完毕。

实现原理

  1. 每一轮从待排序的元素中选择最小(或最大)的元素,将其与序列中的第一个元素交换位置
  2. 再从剩下的元素中选择最小(或最大)的元素,与序列中的第二个元素交换位置
  3. 以此类推,直到整个序列有序
function selectionSort(arr) {
  let len = arr.length;
  let minIndex, temp;
  for (let i = 0; i < len - 1; i++) {
    minIndex = i;
    for (let j = i + 1; j < len; j++) {
      console.log("循环找最小的值", arr[j], arr[minIndex], minIndex);
      if (arr[j] < arr[minIndex]) {
        // 寻找最小的数
        minIndex = j; // 将最小数的索引保存
      }
    }
    // 将当前遍历的项和查找到的最小项交换位置
    temp = arr[i];
    arr[i] = arr[minIndex];
    arr[minIndex] = temp;
  }
  return arr;
}

// 使用
let arr = [3, 1, 5, 7, 2, 4, 6, 8];
console.log(selectionSort(arr)); // [1, 2, 3, 4, 5, 6, 7, 8]

使用场景

由于选择排序的时间复杂度较高,对于大规模数据的排序效率较低,因此在实际应用中并不常用。更适合在小规模或部分有序的数组中使用。

插入排序(Insertion Sort)

插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法

实现原理

通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入

  1. 从第一个元素开始,该元素可以认为已经被排序;
  2. 取出下一个元素,在已经排序的元素序列中从后向前扫描;
  3. 如果该元素(已排序)大于新元素,将该元素移到下一位置;
  4. 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
  5. 将新元素插入到该位置后;
  6. 重复步骤2~5。


    插入排序.gif
function insertionSort(arr) {
    // current 是我们需要插入的当前元素。preIndex 是我们正在与 current 进行比较的元素的位置。
    let preIndex, current;
    for (let i = 1; i < arr.length; i++) {
        preIndex = i - 1;
        current = arr[i];
        while (preIndex >= 0 && arr[preIndex] > current) {
          // 将当前满足的条件的值和当前比较的
          // 当 preIndex 存在且其对应元素值大于 current 时,我们一直将这些元素往后移。
            arr[preIndex + 1] = arr[preIndex];
            preIndex--;
        }
        arr[preIndex + 1] = current;
    }
    return arr;
}
// 示例用法
const array = [4, 2, 2, 8, 3, 3, 10, 5, 6, 2, 3];
const sortedArray = insertionSort(array);
console.log(sortedArray); // 输出: [2, 2, 2, 3, 3, 3, 4, 5, 6, 8, 10]

使用场景

插入排序适用于小规模或部分有序的数组。当待排序数组基本有序时,插入排序的效率较高。它比冒泡排序和选择排序的性能稍好,且实现简单,适用于对于数据量较小的排序任务。插入排序也可以用作其他高级排序算法的子过程,例如快速排序的优化版本中的小数组排序。

希尔排序(Shell Sort)

希尔排序(Shell Sort),也称作缩小增量排序,是插入排序的一种改进版本。它通过将待排序数组分成多个子数组,并对每个子数组进行插入排序,逐步减小子数组的大小,最终完成整个数组的排序。

实现原理

  1. 选择一个初始增量(通常为数组长度的一半)。
  2. 将数组分成多个子数组,每个子数组相隔增量个位置。
  3. 对每个子数组进行插入排序,将子数组中的元素插入到正确的位置。
  4. 逐步减小增量,重复上述步骤,直到增量为1,完成整个数组的排序。


    希尔排序.gif

    希尔排序.gif
 function shellSort(arr) {
    let len = arr.length,
    temp,
    gap = Math.floor(len / 2); // 初始增量
    while (gap > 0) {
        // 对每个子数组进行插入排序
      for (let i = gap; i < len; i++) {
        temp = arr[i];
        let preIndex = i - gap;
        // 在子数组中进行插入排序
        while (preIndex >= 0 && arr[preIndex] > temp) {
          arr[preIndex + gap] = arr[preIndex];
          preIndex -= gap;
        }
        arr[preIndex + gap] = temp;
    }
    gap = Math.floor(gap / 2);
  }
  return arr;
}          
// 示例用法
const array = [4, 2, 2, 8, 3, 3, 10, 5, 6, 2, 3];
const sortedArray = shellSort(array);
console.log(sortedArray); // 输出: [2, 2, 2, 3, 3, 3, 4, 5, 6, 8, 10]

使用场景

希尔排序适用于中等大小的数组排序,特别适用于需要在性能和简单实现之间取得平衡的场景。它在实际应用中常用于对大型数据集进行初步排序的预处理步骤,为后续排序算法提供更好的初始状态。希尔排序相对于其他简单的初级排序算法具有更好的性能,但仍然不如快速排序、归并排序等高级排序算法。

归并排序(Merge Sort)

归并排序(Merge Sort)是一种高效的排序算法,它的原理是将待排序的数组递归地分成两个子数组,分别对子数组进行排序,然后将两个排序好的子数组合并成一个有序的数组。

实现原理

  1. 将待排序数组分割成较小的子数组,直到每个子数组只包含一个元素(递归基线条件)。
  2. 递归地将子数组排序,直到得到排好序的子数组。
  3. 合并排好序的子数组,得到最终有序的数组。


    归并排序.gif
function mergeSort(arr) {
  if (arr.length <= 1) {
    return arr; // 基线条件:数组为空或只有一个元素,直接返回
  }

  const mid = Math.floor(arr.length / 2); // 找到数组的中间位置
  const left = arr.slice(0, mid); // 分割为左子数组
  const right = arr.slice(mid); // 分割为右子数组
    console.log("分组",left,right)
  return merge(mergeSort(left), mergeSort(right)); // 递归地对左右子数组进行归并排序并合并
}

// 合并两个有序数组
function merge(left, right) {
  console.log("获取的数组",left,right)
  const merged = [];
  let leftIndex = 0;
  let rightIndex = 0;

  while (leftIndex < left.length && rightIndex < right.length) {
    if (left[leftIndex] < right[rightIndex]) {
      merged.push(left[leftIndex]); // 左子数组元素较小,放入合并数组
      leftIndex++;
    } else {
      merged.push(right[rightIndex]); // 右子数组元素较小,放入合并数组
      rightIndex++;
    }
  }

  // 将剩余的元素放入合并数组
  return merged.concat(left.slice(leftIndex)).concat(right.slice(rightIndex));
}

// 示例用法
const array = [4, 2, 2, 8, 3, 3, 10, 5, 6, 2, 3];
const sortedArray = mergeSort(array);
console.log(sortedArray); // 输出: [2, 2, 2, 3, 3, 3, 4, 5, 6, 8, 10]

使用场景

归并排序适用于大规模数据的排序,特别适用于外部排序,即排序数据量大到无法全部加载到内存中的场景。归并排序的优势在于排序速度较快且稳定,对于处理大规模乱序数据或需要保持相同元素相对顺序的排序任务非常有效。

堆排序(Heap Sort)

堆排序(Heap Sort)是一种高效的排序算法,它利用堆这种数据结构进行排序。堆是一种特殊的完全二叉树,它满足堆的性质:对于每个节点,其值大于(或小于)其子节点的值。

实现原理

  1. 构建最大堆:从最后一个非叶子节点开始,依次将数组转换为最大堆,保证每个节点的值大于或等于其子节点的值。
  2. 依次取出堆顶元素:将堆顶元素与最后一个元素交换,然后调整堆,将剩余元素重新构建为最大堆。
  3. 重复上述步骤,直到堆中的所有元素都被取出并排好序。
function heapSort(arr) {
  const len = arr.length;

  // 构建最大堆
  for (let i = Math.floor(len / 2) - 1; i >= 0; i--) {
    console.log("sss===构建堆循环前" + i, arr, len, i);
    heapify(arr, len, i);
    console.log("sss===构建堆循环后" + i, arr, len, i);
  }

  // 依次取出堆顶元素并调整堆
  for (let i = len - 1; i > 0; i--) {
    console.log("sss===调整前" + i, arr, i);
    [arr[0], arr[i]] = [arr[i], arr[0]]; // 将堆顶元素与最后一个元素交换
    console.log("sss===调整交换中" + i, arr, i);
    heapify(arr, i, 0); // 调整堆
    console.log("sss===调整后" + i, arr, i);
  }

  return arr;
}

function heapify(arr, n, i) {
  let largest = i; // 当前节点索引
  const left = 2 * i + 1; // 左子节点索引
  const right = 2 * i + 2; // 右子节点索引

  // 找出最大值的索引
  if (left < n && arr[left] > arr[largest]) {
    largest = left;
  }

  if (right < n && arr[right] > arr[largest]) {
    largest = right;
  }

  // 如果最大值的索引不是当前节点,则交换节点并继续调整堆
  if (largest !== i) {
    [arr[i], arr[largest]] = [arr[largest], arr[i]];
    console.log("sss===内部调用前" + largest, arr, largest);
    heapify(arr, n, largest);
    console.log("sss===内部调用后" + largest, arr, largest);
  }
}

// 示例用法
const array = [4, 2, 2, 8, 3, 3, 5, 6, 2, 3,10];
const sortedArray = heapSort(array);
console.log(sortedArray); // 输出: [2, 2, 2, 3, 3, 3, 4, 5, 6, 8, 10]

使用场景

堆排序适用于大规模数据的排序,尤其适用于需要找出最大或最小的K个元素的问题。由于堆排序的构建最大堆操作具有线性时间复杂度,因此可以在一些特定场景中提供较好的性能,如优先队列的实现、Top K 问题等。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容