手写数据结构 排序算法 篇(上)

简介: 手写数据结构 排序算法 篇

冒泡排序

基本版

function bubbleSort(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
}

改进版冒泡排序的编码实现

function betterBubbleSort(arr) {
    const len = arr.length  
    for(let i=0;i<len;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]]
            }
        }
    }
    return arr
}

设置flag最小优化为O(n)

function betterBubbleSort(arr) {
    const len = arr.length  
    for(let i=0;i<len;i++) {
        // 区别在这里,我们加了一个标志位
        let flag = false
        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
            }
        }
        // 若一次交换也没发生,则说明数组有序,直接放过
        if(flag == false)  return arr;
    }
    return arr
}
时间复杂度
冒泡排序的时间复杂度
我们分最好、最坏和平均来看:
最好时间复杂度:它对应的是数组本身有序这种情况。在这种情况下,我们只需要作比较(n-1 次),而不需要做交换。时间复杂度为 O(n)
最坏时间复杂度: 它对应的是数组完全逆序这种情况。在这种情况下,每一轮内层循环都要执行,重复的总次数是 n(n-1)/2 次,因此时间复杂度是 O(n^2)
平均时间复杂度:这个东西比较难搞,它涉及到一些概率论的知识。实际面试的时候也不会有面试官摁着你让你算这个,这里记住平均时间复杂度是 O(n^2) 即可。
选择排序
function selectSort(arr) {
    // 缓存数组长度
    const len = arr.length
    // 定义 minIndex,缓存当前区间最小值的索引,注意是索引
    let minIndex
    // i 是当前排序区间的起点
    for (let i = 0; i < len - 1; i++) {
        // 初始化 minIndex 为当前区间第一个元素
        minIndex = i
        // i、j分别定义当前区间的上下界,i是左边界,j是右边界
        for (let j = i; j < len; j++) {
            // 若 j 处的数据项比当前最小值还要小,则更新最小值索引为 j
            if (arr[j] < arr[minIndex]) {
                minIndex = j
            }
        }
        // 如果 minIndex 对应元素不是目前的头部元素,则交换两者
        if (minIndex !== i) {
            [arr[i], arr[minIndex]] = [arr[minIndex], arr[i]]
        }
    }
    return arr
}
let arr = [3, 4, 2, 8, 5, 5, 2, 0, 6, 6, 3]
console.log(selectSort(arr));
时间复杂度

选择排序的时间复杂度
在时间复杂度这方面,选择排序没有那么多弯弯绕绕:最好情况也好,最坏情况也罢,两者之间的区别仅仅在于元素交换的次数不同,但都是要走内层循环作比较的。因此选择排序的三个时间复杂度都对应两层循环消耗的时间量级: O(n^2)。

插入排序

function insertSort(arr) {
    // 缓存数组长度
    const len = arr.length
    // temp 用来保存当前需要插入的元素
    let temp
    // i用于标识每次被插入的元素的索引
    for (let i = 1; i < len; i++) {
        // j用于帮助 temp 寻找自己应该有的定位
        let j = i
        temp = arr[i]
        // 判断 j 前面一个元素是否比 temp 大
        while (j > 0 && arr[j - 1] > temp) {
            // 如果是,则将 j 前面的一个元素后移一位,为 temp 让出位置
            arr[j] = arr[j - 1]
            j--
        }
        // 循环让位,最后得到的 j 就是 temp 的正确索引
        arr[j] = temp
    }
    return arr
}
let arr = [3, 4, 2, 8, 5, 5, 2, 0, 6, 6, 3]
console.log(insertSort(arr));

时间复杂度

插入排序的时间复杂度
最好时间复杂度:它对应的数组本身就有序这种情况。此时内层循环只走一次,整体复杂度取决于外层循环,时间复杂度就是一层循环对应的 O(n)。
最坏时间复杂度:它对应的是数组完全逆序这种情况。此时内层循环每次都要移动有序序列里的所有元素,因此时间复杂度对应的就是两层循环的 O(n^2)
平均时间复杂度:O(n^2)


希尔排序
function shellSort(arr) { // 参数为需要排序的数组
  // 获取初始的增量
  let gap = Math.floor(arr.length / 2);
  while (gap >= 1) { // 增量小于1时结束循环
    // 从gap开始遍历,因为插入排序假设第一个是有序的
    for (let i = gap; i < arr.length; i++) {
      let j = i; // 为插入排序从后向前排序提供条件
      // 如果排序的后面的数字小于前面的,交换两个数的位置
      while (arr[j] < arr[j - gap] && j - gap >= 0) {
        [arr[j], arr[j - gap]] = [arr[j - gap], arr[j]]
        // j减小gap从后向前遍历
        j -= gap;
      }
    }
    // 增量每次都减小一半
    gap = Math.floor(gap / 2);
  }
  return arr;
}
let arr = [3, 4, 2, 8, 5, 5, 2, 0, 6, 6, 3]
console.log(shellSort(arr));

时间复杂度

时间复杂度
● (1) 最好情况:序列是正序排列,在这种情况下,需要进行的比较操作需(n-1)次。后移赋值操作为0次。即O(n)
● (2) 最坏情况:O(nlog2n)。
● (3) 渐进时间复杂度(平均时间复杂度):O(nlog2n)

归并排序

function mergeSort(arr) {
    const len = arr.length
    // 处理边界情况
    if (len <= 1) {
        return arr
    }
    // 计算分割点
    const mid = Math.floor(len / 2)
    // 递归分割左子数组,然后合并为有序数组
    const leftArr = mergeSort(arr.slice(0, mid))
    // 递归分割右子数组,然后合并为有序数组
    const rightArr = mergeSort(arr.slice(mid, len))
    // 合并左右两个有序数组
    arr = mergeArr(leftArr, rightArr)
    // 返回合并后的结果
    return arr
}
function mergeArr(arr1, arr2) {
    // 初始化两个指针,分别指向 arr1 和 arr2
    let i = 0, j = 0
    // 初始化结果数组
    const res = []
    // 缓存arr1的长度
    const len1 = arr1.length
    // 缓存arr2的长度
    const len2 = arr2.length
    // 合并两个子数组
    while (i < len1 && j < len2) {
        if (arr1[i] < arr2[j]) {
            res.push(arr1[i])
            i++
        } else {
            res.push(arr2[j])
            j++
        }
    }
    // 若其中一个子数组首先被合并完全,则直接拼接另一个子数组的剩余部分
    if (i < len1) {
        return res.concat(arr1.slice(i))  // concat不会改变原数组
    } else {
        return res.concat(arr2.slice(j))
    }
}
let arr = [3, 4, 2, 8, 5, 5, 2, 0, 6, 6, 3]
console.log(mergeSort(arr));

时间复杂度

我们把每一次切分+归并看做是一轮。对于规模为 n 的数组来说,需要切分 log(n) 次,因此就有 log(n) 轮。
 // 计算分割点
const mid = Math.floor(len / 2)    
// 递归分割左子数组,然后合并为有序数组
const leftArr = mergeSort(arr.slice(0, mid)) 
// 递归分割右子数组,然后合并为有序数组
const rightArr = mergeSort(arr.slice(mid,len))
因此单次切分对应的是常数级别的时间复杂度 O(1)。
再看合并,单次合并的时间复杂度为 O(n)。O(n) 和 O(1) 完全不在一个复杂度量级上,因此本着“抓主要矛盾”的原则,我们可以认为:决定归并排序时间复杂度的操作就是合并操作。
log(n) 轮对应 log(n) 次合并操作,因此归并排序的时间复杂度就是 O(nlog(n))。

手写数据结构 排序算法 篇(下)https://developer.aliyun.com/article/1392188

目录
相关文章
|
2月前
|
算法 数据处理 C语言
C语言中的位运算技巧,涵盖基本概念、应用场景、实用技巧及示例代码,并讨论了位运算的性能优势及其与其他数据结构和算法的结合
本文深入解析了C语言中的位运算技巧,涵盖基本概念、应用场景、实用技巧及示例代码,并讨论了位运算的性能优势及其与其他数据结构和算法的结合,旨在帮助读者掌握这一高效的数据处理方法。
58 1
|
3月前
|
存储 人工智能 算法
数据结构与算法细节篇之最短路径问题:Dijkstra和Floyd算法详细描述,java语言实现。
这篇文章详细介绍了Dijkstra和Floyd算法,这两种算法分别用于解决单源和多源最短路径问题,并且提供了Java语言的实现代码。
106 3
数据结构与算法细节篇之最短路径问题:Dijkstra和Floyd算法详细描述,java语言实现。
|
1天前
|
存储 算法 测试技术
【C++数据结构——树】二叉树的遍历算法(头歌教学实验平台习题) 【合集】
本任务旨在实现二叉树的遍历,包括先序、中序、后序和层次遍历。首先介绍了二叉树的基本概念与结构定义,并通过C++代码示例展示了如何定义二叉树节点及构建二叉树。接着详细讲解了四种遍历方法的递归实现逻辑,以及层次遍历中队列的应用。最后提供了测试用例和预期输出,确保代码正确性。通过这些内容,帮助读者理解并掌握二叉树遍历的核心思想与实现技巧。
14 2
|
2月前
|
存储 算法 搜索推荐
Python 中数据结构和算法的关系
数据结构是算法的载体,算法是对数据结构的操作和运用。它们共同构成了计算机程序的核心,对于提高程序的质量和性能具有至关重要的作用
|
2月前
|
数据采集 存储 算法
Python 中的数据结构和算法优化策略
Python中的数据结构和算法如何进行优化?
|
2月前
|
算法
数据结构之路由表查找算法(深度优先搜索和宽度优先搜索)
在网络通信中,路由表用于指导数据包的传输路径。本文介绍了两种常用的路由表查找算法——深度优先算法(DFS)和宽度优先算法(BFS)。DFS使用栈实现,适合路径问题;BFS使用队列,保证找到最短路径。两者均能有效查找路由信息,但适用场景不同,需根据具体需求选择。文中还提供了这两种算法的核心代码及测试结果,验证了算法的有效性。
123 23
|
2月前
|
算法
数据结构之蜜蜂算法
蜜蜂算法是一种受蜜蜂觅食行为启发的优化算法,通过模拟蜜蜂的群体智能来解决优化问题。本文介绍了蜜蜂算法的基本原理、数据结构设计、核心代码实现及算法优缺点。算法通过迭代更新蜜蜂位置,逐步优化适应度,最终找到问题的最优解。代码实现了单链表结构,用于管理蜜蜂节点,并通过适应度计算、节点移动等操作实现算法的核心功能。蜜蜂算法具有全局寻优能力强、参数设置简单等优点,但也存在对初始化参数敏感、计算复杂度高等缺点。
64 20
|
2月前
|
机器学习/深度学习 算法 C++
数据结构之鲸鱼算法
鲸鱼算法(Whale Optimization Algorithm,WOA)是由伊朗研究员Seyedali Mirjalili于2016年提出的一种基于群体智能的全局优化算法,灵感源自鲸鱼捕食时的群体协作行为。该算法通过模拟鲸鱼的围捕猎物和喷出气泡网的行为,结合全局搜索和局部搜索策略,有效解决了复杂问题的优化需求。其应用广泛,涵盖函数优化、机器学习、图像处理等领域。鲸鱼算法以其简单直观的特点,成为初学者友好型的优化工具,但同时也存在参数敏感、可能陷入局部最优等问题。提供的C++代码示例展示了算法的基本实现和运行过程。
64 0
|
3月前
|
机器学习/深度学习 存储 缓存
数据结构与算法学习十:排序算法介绍、时间频度、时间复杂度、常用时间复杂度介绍
文章主要介绍了排序算法的分类、时间复杂度的概念和计算方法,以及常见的时间复杂度级别,并简单提及了空间复杂度。
56 1
数据结构与算法学习十:排序算法介绍、时间频度、时间复杂度、常用时间复杂度介绍
|
2月前
|
算法 vr&ar 计算机视觉
数据结构之洪水填充算法(DFS)
洪水填充算法是一种基于深度优先搜索(DFS)的图像处理技术,主要用于区域填充和图像分割。通过递归或栈的方式探索图像中的连通区域并进行颜色替换。本文介绍了算法的基本原理、数据结构设计(如链表和栈)、核心代码实现及应用实例,展示了算法在图像编辑等领域的高效性和灵活性。同时,文中也讨论了算法的优缺点,如实现简单但可能存在堆栈溢出的风险等。
70 0