你应该烙在心上的基础算法

474 阅读3分钟

这是我参与8月更文挑战的第7天,活动详情查看:8月更文挑战

前言

自己的算法有些许的薄弱,决定开撕算法。

望自己能够坚持下去!!!

二分查找

定义

小时候应该玩过一种猜字游戏: 小朋友 A 心里想着一个 1 到 100 之前的数字,小朋友 B 需要猜到 A 心中的这个字,在猜的过程中 A 会提示 B 猜的数字是大了还是小了。

那么在猜的过程中怎么才能最快的猜到呢?有了这个问题,二分查找的定义也就产生了:

在一个有序的数组中需要找到某一个目标值的时候,可以每次都去判断中间的那个值是不是目标值,如果是则返回;如果不是则用这个值和数组的开头的值或者结束的值组成一个新的区间,然后在这个新区间中再次判断中间值。一直循环这个操作,直到找到或者找不到为止。

场景

在一个有序数组中,查找某目标个值是否存在这个数组中,如果存在则将其对应的索引返回,反之返回一个 -1

流程

  1. 根据二分的定义,需要定义一个开始索引和一个结束索引,用来表示动态区间
  2. 取区间的中间值,如果中间值等于目标值则 return 其索引并结束循环
  3. 如果中间值大于目标值,则说明目标值在中间值和第一个值之间
  4. 如果中间值小于目标值,则说明目标值在中间值和最后一个值之间
  5. 经过 4 或者 5 的判断则可以形成一个新的区间,然后在这个新的区间重复 2 - 4

图解

image-20210830202732623

代码实现

 function binarySearch(list, target) {
   let startIndex = 0
   let endIndex = list.length - 1
   while(startIndex <= endIndex) {
     let middleIndex = Math.floor((startIndex + endIndex) / 2)
     let middleValue = list[middleIndex]
     if (middleValue === target) {
       return middleIndex
     } else if (middleValue > target) {
       endIndex = middleIndex - 1
     } else if (middleValue < target) {
       startIndex = middleIndex + 1
     }
   }
   return -1
 }

问题

  • 为什么在 middleValue > targetmiddleValue < target 中要对 middleIndex 进行减一、加一?

    因为当前中间值已经比较过了,并不等于目标值,所以需要进行加一减一让他指向新的值

  • while 循环的条件可不可以是 startIndex < endIndex

    不可以,因为 startIndex 是会等于 endIndex 的。

  • 取中间值的 (startIndex + endIndex) / 2 为什么要向下取整?

    如果不取整则数组 length 为偶数的时候除以 2 就会出现小数;

    如果想上取整,则会出现大于数组长的索引,那个时候会取不到值。

关键字

设置动态区间,遍历动态区间,取区间中间值和 target 判断。

冒泡排序

流程

  1. 从第一个元素开始比较数组中相邻的两个元素
  2. 如果前边的大于后边的那个则互换位置,反之保持不变
  3. 一直重复 2 ,直到数组的最后一个
  4. 当比较完成之后数组的最后一个肯定就是数组中最大的了
  5. 开启下一轮循环,重复 1 - 5 直到数组所有元素比较完成
  6. 5 执行完成之后只能确定最后一个值的位置,其他值还需要重新进行比较,所以需要在外层套一个循环来控制循环次数

图解

image-20210830202823577

代码实现

 function bubbleSort(list) {
   for (let i = 0; i < list.length - 1; i++) {
     for (let k = 0; k < list.length - 1 - i; k++) {
       if (list[k] > list[k + 1]) {
         const temp = list[k]
         list[k] = list[k + 1]
         list[k + 1] = temp
       }
     }
   }
   return list
 }

问题

  • 外层循环的条件中为什么是 i < list.length - 1 ?

    按照图解中的 demo 来说,条件如果是 i < list.length 的话,则相当于是 i < 8,结束条件前的最后一次循环就是 i = 7,当为 7 遍历的时候这个数组的顺序在 i = 6 的时候就已经是正确的了,所以 i = 7 的时候循环相当于是空循环了一次,所以干脆就直接不循环了。

  • 内层循环为什么要 - i ?

    当第一次比较完成之后,数组的最后一个肯定是最大的了,所以在下次就不需要比对最后一个。

    第一次是最后一个不需要比较,第二次是最后两个不需要...

    最终可以得出: 外层循环的次数就是已经比对过的次数,也就是数组后边不需要比对的个数,所以将其减去。

关键字

相邻的两个元素挨个比较,每次只能确定最后一个值的位置。

选择排序

流程

  1. 将第一个值的索引存起来,假设其对应值为最小值
  2. 循环数组用最小值依次和最小值后边的值进行比较
  3. 期间如果有小于最小值的,则将之前的最小值的索引进行替换
  4. 循环完成之后如果初始化时存的最小值不等于最终的最小值,则将两个位置进行替换
  5. 4 完成之后则可以确定最小值的位置
  6. 重复 1 - 5 ,只不过这次需要将第二个值的索引存起来。

图解

image-20210830202849691

代码实现

 function selectSort(list) {
   for (let i = 0; i < list.length - 1; i++) {
     let cache = i
     for (let k = i + 1; k < list.length; k++) {
       if (list[k] < list[cache]) {
         cache = k
       }
     }
     if (cache !== i) {
       const temp = list[cache]
       list[cache] = list[i]
       list[i] = temp
     }
   }
   return list
 }

注意点

 if (list[k] < list[cache]) {
   cache = k
 }

在个判断中取到更小的值之后没有结束内层循环,而是继续遍历,这里不可以将内层循环结束,因为有可能当前找到值并不是最小的,最小的有可能还在后边。

关键字

选择第一个为最小值,遍历其后边的值最终将最小值优先确定。

插入排序

流程

  1. 从第二个开始依次往后作为假定最小值 cache
  2. 从 cache 开始往左挨个比较
  3. 当左侧某个值比 cache 大时继续向左比较
  4. 如果左侧没有值了,则将 cache 和比 cache 大的那个值互换位置
  5. 如果左侧还有值,继续向左比较,直到左侧没有值了,或者遇到比 cache 小的,则将最后一个比 cache 大的值和 cache 互换位置
  6. 重复 1 - 5,只不过在第一步中要将第三个值作为假定最小值 cache

图解

image-20210830205539342

代码实现

 function insertSort(arr) {
   let length = arr.length;
   for(let i = 1; i < length; i++) {
     let temp = arr[i];
     let j = i
     for(; j > 0; j--) {
       if(temp >= arr[j-1]) {
         break;
       }
       arr[j] = arr[j-1];
     }
     arr[j] = temp;
   }
   return arr;
 }

左侧没有值了在代码中就是 k = 0 的时候。

问题

  • arr[j] = temp 能不能写成 arr[i] = temp ?

    不可以

    1. 执行到这里的条件就是 j=== 0 或者左边那个元素没有缓存的那个值大( arr[index - 1] > temp ),一个是要插入到数组的开头,一个是为了保持原有位置不动。
    2. 因为 i 是当前比较的元素的索引,而 j 是比对之后的索引。在比对的过程中有可能 j 向左移动了好几次,缓存中的值插入的时候就不是 i 对应的位置了

关键字

将第二个值开始依次缓存,每次去和左边的值比较。

快速排序

核心是通过递归来实现,不断的找基准值建立新的分区,不断地在分区中找基准值。

流程

  1. 在数组中随意找一个基准值,如果数组只有一个元素或者为空则直接 return
  2. 遍历数组中除了这个基准值以外的其他元素
  3. 大于基准值的放到 rightArr 中,小于基准值的放到 leftArr
  4. leftArrrightArr 分别来执行 1 - 4
  5. leftArr 执行结果 + 基准值 + rightArr 执行结果返回

图解

image-20210830220338939

代码实现

 function quickSort(list) {
   if (list.length <= 1) return list
   const pivotIndex = list.length - 1
   const pivot = list.splice(pivotIndex, 1)[0]
   const leftArr = []
   const rightArr = []
   for (let i = 0; i < list.length; i++) {
     const item = list[i];
     if (item > pivot) {
       rightArr.push(item)
     } else if (item < pivot) {
       leftArr.push(item)
     }
   }
   
   return quickSort(leftArr).concat([pivot], quickSort(rightArr))
 }

关键字

以递归来实现,不断地确定基准值和分区。

源码地址

learn-algorithm-javascript