排序算法的学习

108 阅读6分钟

一、编程的概念

编程语言不难,难的是抽象概念

  • 初学看不懂是因为没有学习抽象知识

  • 把变成编程语言抽象了。几种编程语言根本就没有区别

  • 首先理解这些语言的不变之处

所有编程都是在写逻辑

三段论逻辑

条件1: 水果有热带的,有温带的

条件2: 香蕉不是温带的

推论:如果命题1和命题2都是ture,那香蕉就是热带的

逻辑和直觉

  • 直觉可以快速让人学会一些东西,又可以阻止人学会另一些反直觉的东西,这时只能依靠逻辑说服自己

  • 如果把所有的推理结果都否决了,那么最后剩下的一种可能不管多离奇都是事情的真想————福尔摩斯

几乎所有语言都能用最少三种语句指定所有逻辑(但错误处理很麻烦)

顺序执行语句

  • 按从上往下的顺序执行代码

条件判断语句

  • if...then...else...

  • if...lese if... else...

循环语句

  • while...do...

  • for i from 1 to n...

二、流程图与伪代码

伪代码

  • 伪代码可以让你不被编程语言所束缚,你自己可以发明API,只要能把逻辑理清就行

  • 伪代码可以作为在写代码前的演算,如果伪代码都写不好,那代码就更写不好

  • 伪代码不需要运行在计算机里,伪代码是运行在脑袋里的逻辑

流程图

  • 锻炼大脑,把逻辑画出来

判断ab两个数谁大.png

多个数判断谁大.png

三、算法前置学习

如何找到两个数中最小的那个

  • 用数组[a,b]表示两个数字

普通写法

let minOf2 = (numbers) => { 
    if (numbers[0] < numbers[1]) {
        return numbers[0]
    } else {
        return numbers[1]
    }
}
  • 默认numbers是长度为2的数组,比较下标0和下标1的两个数字谁大,然后返回小的那一个数字

用问号冒号表达式优化

let minOf2 = numbers =>
    numbers[0] < numbers[1] ? numbers[0] : numbers[1]

用析构赋值优化代码

let minOf2 = ([a, b]) => a < b ? a : b

如何找到三个数中最小的那个

  • 用数组[a,b,c]表示三个数字
let minOf3 = ([a, b, c]) => {
    return minOf2([minOf2([a, b]), c])
}
//或者
let minOf3 = ([a, b, c]) => {
    return minOf2(a, minOf2([a, b]))
}
  • 把三个数拆解成两个数字的数组和一个数字的数组,分别对两个数组进行minOf2()的操作就能得到三个数的最小值

  • 采用了递归

从上面推理出四个数中找到最小的那个

let minOf4 = ([a, b, c, d]) => {
    return minOf2([a, minOf3(b, c, d)])
}
  • 思路和上面一样,任意长度的最小值都可以用 minOf2()实现

JS内置了最小值APIMath.min()

  • Math.min(1,2) // 1

  • Math.min.call(null,1,2)

  • Math.min.apply(null,[1,2])

关于Math

  • 虽然Math看起来像Object一样的构造函数,但它只是普通对象

  • 这是唯一的特例: 首字母大写不是构造函数

递归

  • 所有递归都能用循环改写

特点

  • 函数不停调用自己,每次调用的参数略有不同

  • 当满足某个简要条件时,实现一个简单的调用

  • 最终算出结果

理解

求最小值的递归函数图解.png

四、排序算法

选择排序

  • 可以用递归实现也可以用循环实现

两个数排序

let sort2 = ([a, b]) => {
    if (a < b) {
        return [a, b]
    } else {
        return [b, a]
    }
}
// 优化一下
let sort2 = ([a, b]) => {
    a < b ? [a, b] : [b, a]
}

思路是把两个数相比较,如果第一个数比第二个数小,直接返回原数组,反之调换两个数的顺序后返回

三个数排序

// 最小值函数
let min = (numbers) => {
    if (numbers.length > 2) {
        return min(
            [numbers[0], min(numbers.slice(1))]
        )
    } else {
        return Math.min.apply(null, numbers)
    }
}

// 获取最小值下标函数
let minIndex = (numbers) =>
    numbers.indexOf(min(numbers))
    
// 排序函数
let sort3 = (numbers) => {
    let index = minIndex(numbers)
    let min = numbers[index]
    numbers.splice(index, 1) // 从numbers删掉min
    return [min].concat(sort2(numbers))
}

先获取最小值,然后获取这个最小值在数组里的下标,把最小值从数组内删掉,用最小值拼接数组内剩余数字继续调用sort3函数

任意数字的排序

let sort = (numbers) => {
    if (numbers.length > 2) {
        let index = minIndex(numbers)
        let min = numbers[index]
        numbers.splice(index, 1)
        return [min].concat(sort(numbers))
    } else {
        return numbers[0] < numbers[1] ? numbers : numbers.reverse()
    }
}

思路和上面的思路差不多,先获取最小值以及下标,从原数组内单独拿出来,数组内剩余数字继续调用函数,如果数组内剩余数字小于两个,就直接用两个数字的排序

快速排序

思路

  • 想象你是一个体育委员,面对的同学为一个数组分别是[12,3,7,21,5,9,4,6]

  • 以任意一个数为基准,比它小的数去前面,比它大的数去后面

  • 重复这个操作就可以排序

快速排序.png

阮一峰老师写的代码如下

let quickSort = arr => {
    if (arr.length <= 1) {
        return arr
    }
    let pivotIndex = Math.floor(arr.length / 2)
    let pivot = arr.splice(pivotIndex, 1)[0]
    let left = []
    let right = []
    for (let i = 0; i < arr.length; i++) {
        if (arr[i] < pivot) {
            left.push(arr[i])
        } else {
            right.push(arr[i])
        }
    }
    return quickSort(left).concat([pivot], quickSort(right))
}
  • 声明一个变量把排序数组中间值的下标保存起来

  • 再声明一个变量把这个下标对应的数组从原数组中取出来

  • 声明两个空数组,一个用来存比它小的数,一个存比它大的数

  • 进行for循环如果原数组的数字比它大就push进大数组,反之亦然

  • 然后重复对左边数组和右边数组进行同样的操作,但要把中间的这个比较的数字加起来

  • 如果发现这个数组的长度已经等于1了那么就直接返回它自己,最后得到的数组就是排好序的数组

归并排序

思路和快速排序差不多

  • 你还是那个体育委员,还是面对一样的数组

  • 但这次你把他们分成两半,让左边的人自己排序,让右边的人也自己排序

  • 排好序后再让左边右边的数字合并起来(merge)

merge.png

归并排序.png

代码

let mergeSort = arr => {
    if (arr.length === 1) {
        return arr
    }
    let left = arr.slice(0, Math.floor(arr.length / 2))
    let right = arr.slice(Math.floor(arr.length / 2))
    return merge(mergeSort(left), mergeSort(right))
}
let merge = (a, b) => {
    if (a.length === 0) return b
    if (b.length === 0) return a
    if (a[0] > b[0]) {
        return [b[0]].concat(merge(a, b.slice(1)))
    } else {
        return [a[0]].concat(merge(a.slice(1), b))
    }
}
  • 其实归并排序重点不是上面的mergeSort函数,而是下面的merge函数,因为排序的工作是下面的merge函数做的

  • 先声明两个变量,把原数组切成两半分别存进去

  • 然后对两个数组重复执行上面的操作,然后再用merge对操作后的结果进行操作

  • 在merge里要首先判断两个数组的长度是否为0,如果为0就直接返回另一个数组

  • 如果不为0继续进行判断,看左边数组的第一个数字和右边数组的第一个数字谁小,然后把他们拼接起来返回出去

  • 返回后继续进行merge操作直到他们的长度为0,就代表数组排序完成了

计数排序

思路

  • 类似于把扑克牌进行排序操作,把每个数字对应的四张牌进行排序

  • 用一个哈希表作记录

  • 发现数组N就记N:1,如果再次发现N,就让N的value加1

  • 最后把哈希表的key全部打出来,假设N的value是M,那么就需要把N打印M次

计数排序.png

特点

  • 数据结构不同,这个排序使用了哈希表

  • 只遍历一次数组(不过还要遍历一次哈希表)

  • 这个操作叫用空间换时间

代码

let countSort = arr => {
    let hashTable = {}
    max = 0
    result = []
    for (let i = 0; i < arr.length; i++) { // 遍历数组
        if (!(arr[i] in hashTable)) {
            hashTable[arr[i]] = 1
        } else {
            hashTable[arr[i]] += 1
        }
        if (arr[i] > max) {
            max = arr[i]
        }
    }
    for (let j = 0; j <= max; j++) { // 遍历哈希表
        if (j in hashTable) {
            for (let i = 0; i < hashTable[j]; i++) {
                result.push(j)
            }
        }
    }
    return result
  • 声明一个空的对象当做哈希表,声明一个最大值,初始化为0,声明一个空数组用来打印哈希表内的key

  • 遍历数组

  • 判断如果原数组被遍历到的数字不存在于哈希表中,那哈希表内就要新增这个数字并把value值初始化为1,如果存在 于哈希表内,就把这个数字的value加1

  • 还让每一个被遍历到的数字和max相比,得到原数组中的最大值

  • 最后需要从0到原数组中最大值之间遍历哈希表,这里需要判断这个遍历的j是否存在于哈希表中

  • 如果存在就把这个哈希表存的key遍历并push进result空数组中,完成排序

  • 可以再确定一个原数组中的最小值,遍历哈希表时可以通过最小值到最大值进行遍历,节约内存

算法时间复杂度

  • 选择排序 O(n²)

  • 快速排序 O(log2n)

  • 归并排序 O(log2n)

  • 计数排序 O(n+max)

算法学习总结

  • 脑壳头想,想不出来画图,写伪代码

  • 拿键盘敲,运行,出错,调试,再运行,直到没有错

  • 有些细节很难想清楚, 动手列表格找规律

  • 多画表,多画图,多log

  • 如果js代码有问题,可以先用伪代码