数组排序专题 ——关于算法的学习

206 阅读12分钟

上一次我们讲解了数组中关于去重有哪些方法,今天我们来讲讲数组中关于排序的一些方法。

1. sort

我们先来讲解第一种数组排序方法——sort,JS官方打造的专门用于数组排序的方法。它是这样使用的。

let arr = [2, 4, 7, 3, 1, 6, 5]
arr.sort()

我们有一个数组arr,直接arr调用sort方法就行,它默认是升序输出。

image.png

如果我们想让它降序排序,可以这样写:sort接收一个回调函数,函数里面有两个参数,a和b。当我们return a - b 时,它就会升序输出;当我们return b - a 时,它就会降序输出。

let arr = [2, 4, 7, 3, 1, 6, 5]
arr.sort((a, b) => {
    return b - a
})

image.png

sort方法是直接改动原数组,也叫原地排序,它不会生成一个新数组。

2. 冒泡排序法

除了使用官方为我们打造的排序方法外,我们还可以自己来写一个排序函数。我们先来介绍一种很经典的排序方法——冒泡排序。

冒泡排序的思想是什么呢?我们还是拿着数组arr = [2, 4, 7, 3, 1, 6, 5] 来举个例子。

我左手拿着2右手拿着4去比较,发现2小于4,就不动,然后右手去拿7和左手的2比较,发现2也小于7,也不动,直到碰到左手拿着2右手拿着1,此时,2大于1,我就将2与1交换位置,1就到最开头去了,再左手拿着1右手继续拿着剩余的去比较,如果左手大于右手,我就交换,否则就不动。第一遍遍历我们就能把数组中最小的元素放在0号位,下一次就从4开始去比较。这样我们就能不断地将数组中元素最小的值排到左边,就像冒泡一样慢慢浮起来。

了解了算法的思想,代码是不是就呼之欲出了。用双重for循环就能解决吧。外层循环遍历左手上的值,里层循环遍历右手上的值。

let arr = [2, 4, 7, 3, 1, 6, 5]

function bubble(arr) {
    for (var i = 0; i < arr.length; i++) {
        for (var j = i + 1; j < arr.length; j++) {
            
        }
    }
    return arr
}

console.log(bubble(arr));

外层循环从0开始,里层循环就从1开始咯,所以就 i = 0, j = i + 1。

然后我们去判断左手的值是否大于右手的值。如果大于,就交换位置,否则不动。

let arr = [2, 4, 7, 3, 1, 6, 5]

function bubble(arr) {
    for (var i = 0; i < arr.length; i++) {
        for (var j = i + 1; j < arr.length; j++) {
            if (arr[j] < arr[i]) {
                [arr[i], arr[j]] = [arr[j], arr[i]]
            }
        }
    }
    return arr
}

console.log(bubble(arr));

我们可以去定义一个中间量去交换数组中的两个值,也可以这样写:[arr[i], arr[j]] = [arr[j], arr[i]] 。一个小语法去交换数组中的两个值,应该很好理解。

我们运行一下看看:

image.png

排序成功了。因为是两次for循环,时间复杂度就是 n2n2

这种写法,当数组是以最优情况出现时,也就是排好序的情况下,时间复杂度也是 n2n2。它还有另外一种写法,刚刚我们是将每一次的最小值排到最左边,我们也可以将每一次的最大值排到最右边吧。

我们这样写:

let arr = [2, 4, 7, 3, 1, 6, 5]


function bubble(arr) {
    let n = arr.length
    for (var i = 0; i < n; i++) {
        for (var j = 0; j < n - 1 - i; j++) {

    }
}

console.log(bubble(arr));

我们让里层循环的j等于0,j小于 n - 1 - i。这样写的目的是什么呢?

我们让2与4比较,2小于4,不动;然后就让4与7比较,4小于7,不动;然后让7与3比较,发现7大于3,交换位置;然后让7与1比较,7大于1,交换位置;然后7与6、7与5比较,都大于,交换位置。这样我们就让数组最大值排到了最右边。第一遍比较比较了6次,下一次是不是比较5次就行了,因为7已经跑到最右边了,下一次就不用和7比较了,因为没有元素能大过它。所以j小于 n - 1 - i,j用来控制比较的次数,每遍历一遍比较次数减1。

然后去判断,arr[j] > arr[j + 1] ,左边的大于右边的,就交换位置。

let arr = [2, 4, 7, 3, 1, 6, 5]

function bubble(arr) {
    let n = arr.length
    for (var i = 0; i < n; i++) {
        for (var j = 0; j < n - 1 - i; j++) {
            if (arr[j] > arr[j + 1]) {
                [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]
            }
    }
}

console.log(bubble(arr));

这段代码其实和上一段没有什么区别,只不过是一个向左冒一个向右冒。但这段代码还可以优化,当碰到最优情况,数组已经排好序时,我们可以让时间复杂度降为n。

当这个数组是有序的,它在第一次遍历比较时,是不是if中的语句就走不进来啊,因为左边的不可能大于右边的。这就表明,当第一遍遍历比较时,一次交换语句都没执行,说明这一定是一个有序数组。

那我们就在外面设一个flag为false,在if语句里将flag = true,如果第二层for循环执行完毕后flag的值还是false,说明一次交换语句都没执行,说明它一定是一个有序数组,外面直接返回就行。

let arr = [2, 4, 7, 3, 1, 6, 5]

function bubble(arr) {
    let n = arr.length
    for (var i = 0; i < n; i++) {
        let flag = false
        for (var j = 0; j < n - 1 - i; j++) {
            if (arr[j] > arr[j + 1]) {
                [arr[j], arr[j + 1]] = [arr[j + 1], arr[j]]
                flag = true
            }
        }
        if (!flag) return arr
    }
    return arr
}

console.log(bubble(arr));

这样当数组是有序数组时,它就执行了一遍遍历,所以时间复杂度就为n。这就是冒泡排序的最优写法了。

3. 选择排序法

我们再来讲一种排序方法——选择排序。

在写代码之前。我们先搞清楚这种算法的思想,写代码才会比较好写。

选择排序的思想就是:找到数组中的最小值,把它放在最前面,不断地缩减要找的区间。

let arr = [2, 4, 7, 3, 1, 6, 5]

function selectSort(arr) {
    const len = arr.length
    for (var i = 0; i < len; i++) {
        let minIndex = i
        
    }
    return arr
}

console.log(selectSort(arr));

我们先人为的认定数组中最小值的下标为i,再拿数组中的每一个值与这个下标对应的值比较,如果找到了更小的,我们就将最小值的下标去更新,然后去交换值。

let arr = [2, 4, 7, 3, 1, 6, 5]

function selectSort(arr) {
    const len = arr.length
    for (var i = 0; i < len; i++) {
        let minIndex = i
        for (let j = i; j < len; j++) {
            
        }
        
    }
    return arr
}

console.log(selectSort(arr));

我们让j = i,当第一遍遍历时,找到了最小值放到了0号位,下一次从1号位开始找就行了,所以让j = i,第二次遍历就从1号位开始找。

let arr = [2, 4, 7, 3, 1, 6, 5]

function selectSort(arr) {
    const len = arr.length
    for (var i = 0; i < len; i++) {
        let minIndex = i
        for (let j = i; j < len; j++) {
            if (arr[j] < arr[minIndex]) {
                minIndex = j
            }
        }        
    }
    return arr
}

console.log(selectSort(arr));

然后去遍历数组,当发现找到比此时最小值还要小的值,我们就更新下标值为此时的j。

let arr = [2, 4, 7, 3, 1, 6, 5]

function selectSort(arr) {
    const len = arr.length
    for (var i = 0; i < len; i++) {
        let minIndex = i
        for (let j = i; j < len; j++) {
            if (arr[j] < arr[minIndex]) {
                minIndex = j
            }
        }
        if (minIndex !== i) {
            [arr[i], arr[minIndex]] = [arr[minIndex], arr[i]]
        }       
    }
    return arr
}

console.log(selectSort(arr));

然后当minIndex !== i,我们就去交换数组中的两个值,此时一定找到了更小的值。

这样代码就写完了,还是比较好理解的,我们来看看运行结果:

image.png

同样找到了最小值。这个时间复杂度当然也是 n2n2 了。

4. 插入排序法

我们再来讲一种排序方法——插入排序。

插入排序的思想就是:人为认定数组前一段(只要1个值也算)是有序的,将之后的值想办法放进已经有序的队伍当中。

因为前面那段数组是有序的,我们就将要插入的值与有序数组最后一位比较,如果比它大,就不动;如果比它小,就与有序数组前一位去比较,重复执行,直到找到合适的位置。

let arr = [2, 1, 7, 3, 4, 1, 6, 5]

function insertSort() {
    const len = arr.length
    let target
    for (let i = 1; i < len; i++) {
        target = arr[i]
        
}

console.log(insertSort(arr))

我们定义一个target作为插入值,从1号位开始,这就表明我们认为的将0号位上的2作为了一个有序数组。当数组中只有一个元素时,它当然就是有序的了。

我们此时就要拿着target与前面的有序数组去比较了,那就需要一个一个去比,所以还需要一层遍历,去遍历有序数组。

let arr = [2, 1, 7, 3, 4, 1, 6, 5]

function insertSort() {
    const len = arr.length
    let target
    for (let i = 1; i < len; i++) {
        target = arr[i]
        let j = i
        while (arr[j - 1] > target) {
           
        }
        
}

console.log(insertSort(arr))

因为我们不知道前面有序数组的长度,就不知道要遍历多少次,所以我们就用while循环。

然后我们让 j = i,拿着target与j - 1号位上的值去比较,也就是有序数组最后一位的值。如果最后一位大于target,就与前一位比较。如果找到了合适的位置,应该怎么插入呢?

我们这样插,举个例子给你看看:

假如现在的数组长这样:[1, 3, 2], 我们想把2插入到前面的有序数组中,target就为2了,然后拿target与3比较,发现3大于target,就将此时的2改为3,变成了[1, 3, 3],因为我们已经将2存放到target中去了,所以改掉2也没有问题。然后再拿target与1去比较,发现1小于target,说明找到了合适的位置,就将中间的3改为2,就变成了[1, 2, 3],就成功插入了。

所以只要循环走进来,就让此时的arr[j]等于arr[j - 1]。

let arr = [2, 1, 7, 3, 4, 1, 6, 5]

function insertSort() {
    const len = arr.length
    let target
    for (let i = 1; i < len; i++) {
        target = arr[i]
        let j = i
        while (arr[j - 1] > target) {
            arr[j] = arr[j - 1]
            j--
        }
        
}

console.log(insertSort(arr))

那应该将target插入到哪个下标上呢?对于数组[1, 3, 2],此时i为2,j也为2。然后拿i与j-1比较,能进入循环,于是j--,拿i与j-2去比较,发现不满足循环条件,跳出循环。j此时为1,刚好就是我们要插入的位置。

let arr = [2, 1, 7, 3, 4, 1, 6, 5]

function insertSort() {
    const len = arr.length
    let target
    for (let i = 1; i < len; i++) {
        target = arr[i]
        let j = i
        // 遍历有已经序的值,找出target应在的位置
        while (arr[j - 1] > target) {
            arr[j] = arr[j - 1]
            j--
        }
        arr[j] = target
    }
    return arr
}

console.log(insertSort(arr))

这样代码就写完了,我们来运行一下看看:

image.png

这个方法时间复杂度依然是 n2n2。当出现最优情况,数组本来就是排好序的,那while循环就走不进来,此时时间复杂度就为n了。

5. 快速排序法

我们再来讲一讲今天的最后一种排序方法——快速排序。

那首先我们来讲讲快排的思想。我们有一个数组arr,我先将它的中间值抠出来,7或3都行。假如我取了3。然后去创建两个空数组,然后去遍历原数组,比3小的放左边,比3大的放右边。

let arr = [2, 4, 7, 3, 5, 1]

// [2, 1]    3    [4, 7, 5]

然后去重复这个操作,对左边的数组取一个中间值,再创建两个空数组,小的放左边,大的放右边。右边的数组同理。

let arr = [2, 4, 7, 3, 5, 1]

// [2, 1]    3    [4, 7, 5]
// [] 1  [2]     3      [4. 5]   7   []

然后再去重复执行,直到拆到每一个数组中只有一个值时:

let arr = [2, 4, 7, 3, 5, 1]

// [2, 1]    3    [4, 7, 5]
// [] 1  [2]     3      [4. 5]   7   []
// [] 1  [2]     3      [4]   5  []       7   []

最后将它们拼起来,就得到了一个有序数组了。

理解了思想,我们就来写代码:

let arr = [2, 4, 7, 3, 5, 1]

function quikSort(arr) {
    let middleIndex = Math.floor(arr.length / 2)
    let middle = arr.splice(middleIndex, 1)[0]
    let left = []
    let right = []
    const len = arr.length
   
}

console.log(quikSort(arr))

我们定义middleIndex为中间值的下标,我们向下取整,然后再用splice将这个中间值从原数组中扣下来。splice会返回一个新数组,我们就将这个中间值存放到middle中去。然后再定义一个左数组left,右数组right,len获取扣完中间值的数组长度。

然后去遍历这个数组,拿每一个值与中间值middle比较,比middle小就push到left中去,比middle大就push到right中去。

let arr = [2, 4, 7, 3, 5, 1]

function quikSort(arr) {
    let middleIndex = Math.floor(arr.length / 2)
    let middle = arr.splice(middleIndex, 1)[0]
    let left = []
    let right = []
    const len = arr.length
    for (let i = 0; i < len; i++) {
        if (arr[i] < middle) {
            left.push(arr[i])
        } else {
            right.push(arr[i])
        }
    }
   
   
}

console.log(quikSort(arr))

然后我们得到的left和right是不是要干同样的操作啊,取中间值,创建两个数组,比大小。而我们写的这个函数就是干这个的,所以要用到递归了。

let arr = [2, 4, 7, 3, 5, 1]

function quikSort(arr) {
    let middleIndex = Math.floor(arr.length / 2)
    let middle = arr.splice(middleIndex, 1)[0]
    let left = []
    let right = []
    const len = arr.length
    for (let i = 0; i < len; i++) {
        if (arr[i] < middle) {
            left.push(arr[i])
        } else {
            right.push(arr[i])
        }
    }
    quikSort(left)
    quikSort(right)    
}

console.log(quikSort(arr))

然后我们要将得到的数组进行拼接,就是排好序的左数组、middle、排好序的右数组。我们这样写:

let arr = [2, 4, 7, 3, 5, 1]

function quikSort(arr) {
    let middleIndex = Math.floor(arr.length / 2)
    let middle = arr.splice(middleIndex, 1)[0]
    let left = []
    let right = []
    const len = arr.length
    for (let i = 0; i < len; i++) {
        if (arr[i] < middle) {
            left.push(arr[i])
        } else {
            right.push(arr[i])
        }
    }
   // quikSort(left)
   // quikSort(right)

    return [...quikSort(left), middle, ...quikSort(right)]
}

console.log(quikSort(arr))

我们用解构,放到一个新数组中去,然后返回它。我们还没有设置递归的出口。当进行最后一次递归时,传进来的数组已经只剩下一个值了,我们就不用进行操作了。所以我们在代码的最开始进行一下判断,当传进来的数组长度小于等于1时,直接返回,不用进行接下来的操作了。

let arr = [2, 4, 7, 3, 5, 1]

function quikSort(arr) {
    if (arr.length <= 1) {
        return arr
    }
    let middleIndex = Math.floor(arr.length / 2)
    let middle = arr.splice(middleIndex, 1)[0]
    let left = []
    let right = []
    const len = arr.length
    for (let i = 0; i < len; i++) {
        if (arr[i] < middle) {
            left.push(arr[i])
        } else {
            right.push(arr[i])
        }
    }
   // quikSort(left)
   // quikSort(right)

    return [...quikSort(left), middle, ...quikSort(right)]
}

console.log(quikSort(arr))

这样整段代码就写完了。我们运行一下看看:

image.png

成功排序了。那这段代码的时间复杂度是多少呢?我们每次递归进去数组的长度都减半了,时间也会减半,所以时间复杂度就是 o(nlog(n))。