算法和数据结构之二分

170 阅读5分钟

1.二分查找(折本查找)

一种效率较高的查找方法,但是要求线性表必须采用顺序存储结构,表中元素按关键字有序排列。

// 704 二分查找
//给定一个n个元素有序的(升序)整型数组nums和一个目标值target,写一个函数搜索nums中的targrt,如果目标值存在返回下标,否则返回-1

var search = function(nums,target){
  //开始位置
  let low = 0  
  //结束位置
  let high= nums.length-1
  // 找中间值
  while(low==high){
    //使用位运算避免处理小数
    const mid = low+high>>1
    const num = nums[mid]
    if(num==target){
      return mid
    }else if(num >target){
      high = mid-1
    }else{
      mid =mid+1
    }
  }
  return -1
}

const testCases = [
  { nums: [1, 2, 3, 4, 5], target: 3, expected: 2 }, // 目标值存在的情况
  { nums: [1, 2, 3, 4, 5], target: 6, expected: -1 }, // 目标值不存在的情况
  { nums: [], target: 5, expected: -1 }, // 空数组的情况
];

// 执行算法并验证结果
testCases.forEach((testCase, index) => {
  const result = search(testCase.nums, testCase.target);
  if (result === testCase.expected) {
    console.log(`Test case ${index + 1}: Passed`);
  } else {
    console.log(
      `Test case ${index + 1}: Failed. Expected ${testCase.expected}, but received ${result}`
    );
  }
});

2.二分法插入排序(二分排序)

是在插入第i个元素时,对前面的0~i-1进行折半,先跟他们中间的元素比,如果小,则对前半再折半,否则,对后半进行折半,直到left<right,然后再把第i个元素前一位和目标位置之间的所有元素后移,再把第i个元素放在目标位置上

// 给定一个排序数组和一个目标值,在数组中找到目标值,并返回其所在的索引,如果目标值不存在于数组中,返回它将会被按顺序插入的位置
// 使用时间复杂度为O(log n)的算法

 const  searchInsert  =(nums,target)=>{
    let left = 0, right=nums.length-1,mid,ans =nums.length
    // 边界
    while (left<=right) {
        mid =left+right>>1
        if(target<=nums[mid]){
            ans=mid
            right =mid-1
        }else{
            left =mid+1
        }
    }
    return ans
 }

let nums = [11,12,13,14,15,22,99];
let target = 4;
 
let insertIndex = searchInsert(nums,target);
nums.splice(insertIndex, 0, target);
console.log(nums);

解释:

  1. const searchInsert = (nums, target) => {: 这是一个使用箭头函数语法定义的名为 searchInsert 的函数。它接收两个参数:nums 是已排序的整数数组,target 是要插入的目标值。
  2. let left = 0, right = nums.length - 1, mid, ans = nums.length;: 这一行定义了一些变量,其中: ○ left 和 right 分别表示搜索区间的左右边界; ○ mid 表示搜索区间的中间位置; ○ ans 用于存储插入位置的索引,默认为数组长度,即将目标值插入到数组末尾。
  3. while (left <= right) {: 这是一个循环,它会在 left 小于等于 right 的条件下执行。
  4. mid = left + right >> 1;: 计算中间位置 mid,这里使用位运算右移一位来代替除以 2,以提高效率。
  5. if (target <= nums[mid]) {: 如果目标值小于等于中间值,则执行以下语句。 ○ ans = mid;: 将 ans 更新为 mid,表示插入位置在当前中间值的前面。 ○ right = mid - 1;: 缩小搜索区间的右边界,因为目标值小于等于中间值,说明插入位置在左侧。
  6. else {: 否则,执行以下语句。 ○ left = mid + 1;: 扩大搜索区间的左边界,因为目标值大于当前中间值,说明插入位置在右侧。
  7. }: 循环结束。
  8. return ans;: 返回插入位置的索引。
  9. let nums = [11, 12, 13, 14, 15, 22, 99];: 定义一个已排序的整数数组 nums。
  10. let target = 4;: 定义要插入的目标值 target。
  11. let insertIndex = searchInsert(nums, target);: 调用 searchInsert 函数,得到要插入的位置索引。
  12. nums.splice(insertIndex, 0, target);: 使用数组的 splice 方法,在 insertIndex 处插入目标值 target。
  13. console.log(nums);: 输出经过插入操作后的数组,查看插入结果。 综合起来,这段代码通过二分查找算法确定了将目标值插入已排序数组的正确位置,然后使用 splice 方法在该位置插入目标值,最后输出插入后的数组。

**补充:mid = left + right >> 1 **

解释举例: mid = left + right >> 1 是一种计算中间位置的常见优化方式,通过将加法和位运算结合在一起,可以更高效地计算中间位置。

下面是一个具体的例子来解释这个表达式: 假设 left = 2,right = 6,我们希望计算它们的中间位置。首先,将 left 和 right 相加得到 8,然后通过右移一位(相当于除以 2)来得到最终的中间位置 4。 具体步骤如下:

  1. 将 left 和 right 相加:2 + 6 = 8。
  2. 执行右移一位操作:8 >> 1。
  3. 二进制表示下,8 的二进制为 00001000,右移一位后得到 00000100。
  4. 将二进制转换为十进制,得到中间位置 4。 因此,mid = left + right >> 1 的计算结果为 4,这是 left 和 right 的中间位置。 总结:这种用位运算来计算中间位置的优化方式可以在计算效率方面提供一些性能上的改进,因为位运算比加法运算更高效。然而,需要注意的是,这种优化方式的效果在具体的运行环境和对应的数据规模上可能会有所差异。在实际应用中,我们可以根据实际情况使用这种优化方式,并在必要时进行性能测试和评估。
function binaryInsert(arr, value) {
  let low = 0;
  let high = arr.length - 1;
  
  while (low <= high) {
    let mid = Math.floor((low + high) / 2);
    
    if (arr[mid] === value) {
      return mid + 1; // 插入位置为当前元素的后面
    } else if (arr[mid] < value) {
      low = mid + 1;
    } else {
      high = mid - 1;
    }
  }
  
  return low; // 插入位置为low
}

// 示例用法
let array = [1, 3, 5, 7, 9];
let value = 4;

let insertIndex1 = binaryInsert(array, value);
array.splice(insertIndex1, 0, value);
console.log(array); // 输出 [1, 3, 4, 5, 7, 9]

解释: 将一个值插入已排序的数组中,并保持数组的有序性。函数的作用是找到插入位置,并返回该位置的索引。

首先,函数接受两个参数,一个是已排序的数组 arr,另一个是要插入的值 value。 函数使用 while 循环来迭代查找插入位置。循环的条件是 low 小于等于 high,即仍然存在未查找的区间。 在每次循环中,函数计算 mid,即当前未查找区间的中间位置。然后,函数将 value 与数组中的中间元素 arr[mid] 进行比较。

  • 如果 arr[mid] 等于 value,说明要插入的值已经存在于数组中,此时函数返回 mid + 1,表示插入位置在当前元素的后面。
  • 如果 arr[mid] 小于 value,说明要插入的值应该在当前元素的后面,所以将 low 更新为 mid + 1,继续在右侧查找。
  • 如果 arr[mid] 大于 value,说明要插入的值应该在当前元素的前面,所以将 high 更新为 mid - 1,继续在左侧查找 当循环结束时,找到了插入位置,此时 low 的值就是要插入的位置的索引。函数返回 low。

接下来,我们也展示了一个示例用法: 假设有一个已排序数组 [1, 3, 5, 7, 9],我们想要将值 4 插入到数组中。首先,我们调用binaryInsert 函数,并传入数组和要插入的值,获取到插入位置的索引 insertIndex1。然后,我们使用数组的 splice 方法,在 insertIndex1 的位置插入值 4。最后,我们使用 console.log 输出了插入后的数组 [1, 3, 4, 5, 7, 9]。

时间复杂度: 这个二分插入函数的时间复杂度是 O(log n),其中 n 是已排序数组的长度。

在每次循环迭代中,函数将查找区间减半,因为它只在左半部分或右半部分继续查找,而不是在整个数组中搜索。这就是二分查找的核心思想,每次迭代都将查找区间缩小一半,因此可以在较短的时间内找到插入位置。

由于数组的长度每次都减半,假设初始数组长度为 n,那么在最坏情况下,经过 k 次迭代后,查找区间的长度将变为 n / 2^k。当查找区间的长度缩小到 1 时,循环将终止,因为 low 将等于 high。 所以,我们可以通过以下方程来解决 n / 2^k = 1

*求解 k:k= log2(n) *

因此,循环最多执行 log2(n) 次,这就是函数的时间复杂度。因此,二分插入函数的时间复杂度为 O(log n)。