上一次我们讲解了数组中关于去重有哪些方法,今天我们来讲讲数组中关于排序的一些方法。
1. sort
我们先来讲解第一种数组排序方法——sort,JS官方打造的专门用于数组排序的方法。它是这样使用的。
let arr = [2, 4, 7, 3, 1, 6, 5]
arr.sort()
我们有一个数组arr,直接arr调用sort方法就行,它默认是升序输出。
如果我们想让它降序排序,可以这样写: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
})
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]] 。一个小语法去交换数组中的两个值,应该很好理解。
我们运行一下看看:
排序成功了。因为是两次for循环,时间复杂度就是 。
这种写法,当数组是以最优情况出现时,也就是排好序的情况下,时间复杂度也是 。它还有另外一种写法,刚刚我们是将每一次的最小值排到最左边,我们也可以将每一次的最大值排到最右边吧。
我们这样写:
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,我们就去交换数组中的两个值,此时一定找到了更小的值。
这样代码就写完了,还是比较好理解的,我们来看看运行结果:
同样找到了最小值。这个时间复杂度当然也是 了。
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))
这样代码就写完了,我们来运行一下看看:
这个方法时间复杂度依然是 。当出现最优情况,数组本来就是排好序的,那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))
这样整段代码就写完了。我们运行一下看看:
成功排序了。那这段代码的时间复杂度是多少呢?我们每次递归进去数组的长度都减半了,时间也会减半,所以时间复杂度就是 o(nlog(n))。