排序及二分查找

473 阅读7分钟

二分查找

二分查找的优势

参考

  • 1、最省内存

二分查找算法基于已排序的原数组,属于本地查找算法。而基于二叉堆 / 散列表的查找算法还需要使用额外空间。

  • 2、对数时间复杂度 参考 二分查找的时间复杂度仅为O(lgn)。,时间复杂度计算,由于二分查找每次查找都减半所以为O(log2N),倒过来就是n^(1/2)就是n的一半。所以复杂度为lgN,但是底数不限因为随着数值得增大底数没有意义。

1.4 二分查找的局限性

  • 1、依赖于顺序表

二分查找算法适用于顺序表,而不适用与链表。这是因为顺序表随机访问元素的时间复杂度为O(1),而链表随机访问的时间复杂度为O(n),后者实现二分查找的时间复杂度为O(nlgn),这比O(n)顺序遍历链表还慢。

  • 2、依赖于数据有序

二分查找的必要条件之一是数据有序,否则最低需要O(nlgn)的时间复杂度进行预先排序(快速排序)。如果插入 / 删除操作不频繁,那么排序操作的时间成本可以被多次查找操作的成本均摊。这意味着二分查找适合静态有序的数据类型,或者插入 / 删除不频繁的动态数据数据。否则,应该采用二叉堆等动态数据类型。

  • 3、不适用数据量太大的场景

二分查找依赖于顺序表,意味着存储数据就需要一块连续内存。如果程序的内存不足以分配这样一块连续的数组,那么就无法使用二分查找。

(R - L) >> 1是除于2并向上取整(往小的取),当L=R还没找到结果时,最后L,R两者只有一个会移动,如果是L移动则在右边往大移,R移动则在左边,每次判断移动时只要将目标值放中间,mid在目标值那边那边移动

var searchInsert = function(nums, target) {
    let L = 0
    let R = nums.length - 1
    while(L <= R) {
        let mid = ((R - L) >> 1) + L
        if(nums[mid] == target) {
            return mid
        }else if (nums[mid] < target) {
            L = mid + 1
        }else {
            R = mid - 1
        }
    }
    return L
};

题目

搜索插入位置

leetcode image.png

var searchInsert = function(nums, target) {
    let L = 0
    let R = nums.length - 1
    while(L <= R) {
        let mid = ((R - L) >> 1) + L
        if(nums[mid] == target) {
            return mid
        }else if (nums[mid] < target) {
            L = mid + 1
        }else {
            R = mid - 1
        }
    }
    return L
};

x的平方根 常考

leetcode

image.png

var mySqrt = function(x) {
    let L = 0
    let R = x
    while(L <= R) {
        let mid = ((R - L) >> 2) + L
        if(mid*mid == x){
            return mid
        }else if(mid*mid < x) {
            L = mid + 1
        }else {
            R = mid - 1
        }
    }
    return R
};

寻找峰值

leetcode

image.png

var findPeakElement = function(nums) {
    nums.push(Number.MIN_SAFE_INTEGER)
    nums.unshift(Number.MIN_SAFE_INTEGER)
    let L = 0
    let R = nums.length - 1
    while(L <= R) {
        let mid = Math.floor((R - L) / 2) + L
        if((nums[mid] > nums[ mid + 1]) && (nums[mid] > nums[mid - 1])){
            return mid - 1
        } else  if(nums[mid] > nums[mid + 1]){
            R = mid - 1
        } else {
            L = mid + 1
        }
    }
};

排序

参考 image.png

快速排序

参考 快速排序:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据比另一部分的所有数据要小,再按这种方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,使整个数据变成有序序列。思想类似于二分查找,逐步缩小排序空间。

时间复杂度:平均O(nlogn),最坏O(n2),实际上大多数情况下小于O(nlogn)
空间复杂度:O(logn)(递归调用消耗)

什么时候会出现最坏的情况
快速排序最坏的情况是初始序列已经有序,第1趟排序经过n-1次比较后,将第1个元素仍然定在原来的位置上,并得到一个长度为n-1的子序列;第2趟排序经过n-2次比较后,将第2个元素确定在它原来的位置上,又得到一个长度为n-2的子序列。这个时候为O(n^2)

    function quickSort(array, start, end) {
      if (end - start < 1) {
        return;
      }
      const target = array[start];
      let l = start;
      let r = end;
      while (l < r) {
        while (l < r && array[r] >= target) {
          r--;
        }
        array[l] = array[r];
        while (l < r && array[l] < target) {
          l++;
        }
        array[r] = array[l];
      }
      array[l] = target;
      quickSort(array, start, l - 1);
      quickSort(array, l + 1, end);
      return array;
    }

插入排序

参考 image.png

插入排序适合大部分已经被排好序的数据,每次往后取一个未排序的数字,插入到前面已排序的数组中。

时间复杂度:O(n2) 空间复杂度:O(1)

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

冒泡排序

循环数组,比较当前元素和下一个元素,如果当前元素比下一个元素大,向上冒泡。

这样一次循环之后最后一个数就是本数组最大的数。

下一次循环继续上面的操作,不循环已经排序好的数。

优化:当有一次循环没有发生冒泡,说明已经排序完成,停止循环

    function bubbleSort(array) {
      for (let j = 0; j < array.length; j++) {
        let flag = true;
        for (let i = 0; i < array.length - 1 - j; i++) {
          // 比较相邻数
          if (array[i] > array[i + 1]) {
            [array[i], array[i + 1]] = [array[i + 1], array[i]];
            flag = false;
          }
        }
        // 没有冒泡结束循环
        if (complete) {
          break;
        }
      }
      return array;
    }

选择排序

每次循环选取一个最小的数字放到前面的有序序列中。 时间复杂度:O(n2) 空间复杂度:O(1)

    function selectionSort(array) {
      for (let i = 0; i < array.length - 1; i++) {
        let minIndex = i;
        for (let j = i + 1; j < array.length; j++) {
          if (array[j] < array[minIndex]) {
            minIndex = j;
          }
        }
        [array[minIndex], array[i]] = [array[i], array[minIndex]];
      }
    }

归并排序

参考
利用归并的思想实现的排序方法。 该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。(分治法将问题分成一些小的问题然后递归求解,而治的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。

步骤

  1. 将序列中带排序数字分为若干组,每个数字分为一组
  2. 将若干个组两两合并,保证合并后的组是有序的(两两合并时比较两个组的头部谁更小,小的那个移到合并的数组里,当其中一个组没有值时,另一个组剩余的数放在合并数组的后面)
  3. 重复第二步操作直到只剩下一组,排序完成 image.png
    function mergeSort(array) {
      if (array.length < 2) {
        return array;
      }
      const mid = Math.floor(array.length / 2);
      const front = array.slice(0, mid);
      const end = array.slice(mid);
      return merge(mergeSort(front), mergeSort(end));
    }

    function merge(front, end) {
      const temp = [];
      while (front.length && end.length) {
        if (front[0] < end[0]) {
          temp.push(front.shift());
        } else {
          temp.push(end.shift());
        }
      }
      while (front.length) {
        temp.push(front.shift());
      }
      while (end.length) {
        temp.push(end.shift());
      }
      return temp;
    }

堆排序

参考

对于一个堆来说,要符合以下两个特点

  1. 是一个完全二叉树
  2. 所以父节点的值都要大于(或小于)子节点的值

完全二叉树的性质

  1. 如果父元素为i, 则左子树为2i,右子树为2i+1。下标从零开始(jsi + 1 = i),左子树为2i+1,右子树为2i+2。
  2. 第n层的节点数最多为2^n个节点, 从0开始算层
  3. n层二叉树最多有2^n+1 - 1个节点
  4. 第一个非叶子节点:length/2,下标从零开始length/2 - 1,向下取整

大顶堆升序

    function heapSort(array) {
      creatHeap(array);
      //console.log(array);
      // 交换第一个和最后一个元素,然后重新调整大顶堆
      for (let i = array.length - 1; i > 0; i--) {
        [array[i], array[0]] = [array[0], array[i]];
        adjust(array, 0, i);
      }
      return array;
    }
    // 构建大顶堆,从第一个非叶子节点开始,进行下沉操作
    function creatHeap(array) {
      const len = array.length;
      const start = Math.floor(len / 2) - 1;//向下取整
      for (let i = start; i >= 0; i--) {
        adjust(array, i, len);
      }
    }
    // 将第target个元素进行下沉,孩子节点有比他大的就下沉
    function adjust(array, target, len) {
      for (let i = 2 * target + 1; i < len; i = 2 * i + 1) {
        // 找到孩子节点中最大的
        if (i + 1 < len && array[i + 1] > array[i]) {
          i = i + 1;
        }
        // 下沉
        if (array[i] > array[target]) {
          [array[i], array[target]] = [array[target], array[i]]
          target = i;
        } else {
          break;
        }
      }
    }

问道

最小的k个数

题目 image.png 小顶堆

function GetLeastNumbers_Solution(input, k)
{
    // write code here
    function heapSort (input, k) {
        function adjust (input, target, len) {
           for(let i = 2 * target + 1; i < len; i = 2 * i + 1) {
               if (i + 1 < len && input[i] > input[i + 1]) {
                   i = i + 1
               }
               if( input[target] > input[i] ) {
                   [input[target], input[i]] = [input[i], input[target]]
                   target = i;
               }else {
                   break;
               }
           }
        }
        
        function creatHeap (input) {
            let len = input.length
            let start = Math.floor(len / 2) - 1
            for(let i = start; i >= 0; i--) {
                adjust(input, i, len)
            }
        }
        let res = []
        creatHeap(input)
        for(let i = 0; i <  k; i++) {
            let lem = input.length - 1;
            [input[lem], input[0]] = [input[0], input[lem]];
            res.push(input.pop())
            adjust(input, 0, lem)
        }
        return res
    }
    
    return heapSort(input, k)
}
module.exports = {
    GetLeastNumbers_Solution : GetLeastNumbers_Solution
};

合并两个有序数组

image.png 思路:利用归并排序的思路,比较两数组第一个值,但是这里由于需要存储在nums1的数组中,有两种方式一种直接将nums1数组作为存储的数组把比较的值直接覆盖。另一种方式由于nums1后面是0,所以可以找最大值然后依次放入nums1从后往前放。

var merge = function (nums1, m, nums2, n) {
  let l = m - 1
  let r = n - 1
  let target = m + n - 1
  while(l >= 0 || r >= 0) {
      if(l === -1){   
          nums1[target--] = nums2[r--] 
      }else if(r === -1) {
         return
      }else if(nums1[l] >= nums2[r]) {
          nums1[target--] = nums1[l--]
      }else{
          nums1[target--] = nums2[r--]
      }
  }
};

多数元素

题目

image.png 法一:排序后找中间的数,因为出现次数大于n/2

var majorityElement = function(nums) {
    nums.sort()
    return nums[Math.floor(nums.length / 2)]
};

法二:哈希值,最稳

var majorityElement = function(nums) {
    let map = new Map()
    let max = 0, maxName = nums[0]
    for(let i = 0; i < nums.length; i++) {
        if(map.has(nums[i])) {
            map.set(nums[i], map.get(nums[i]) + 1)
            if(max < map.get(nums[i])) {
                max = map.get(nums[i])
                maxName = nums[i]
            }
        }else {
            map.set(nums[i], 1)
        }
    }
    return maxName
};

法三:Boyer-Moore 投票算法, 由于是众数且次数大于n/2那么会比其他的数多,所以如果遇到不一样的数max--,将这个数抵消,如果max=0,就令maxName为当前的数。这样最后就会得到那个众数

var majorityElement = function(nums) {
    let max = 0, maxName = nums[0]
    nums.forEach(item => {
        if(max === 0) {
            maxName = item
            max = 1
        }else if(item === maxName) {
            max++
        }else {
            max--
        }
    })
    return maxName
};

有效的字母异位词

题目

image.png

var isAnagram = function(s, t) {
    return s.length === t.length && [...s].sort().join('') === [...t].sort().join('')
};

三数之和

image.png 思路: 这道题最容易想到的是三重循环,而三重循环的时间复杂度太高,可以进一步优化,利用排序+双指针优化到O(N^2)
排序之后的值是从小到大的,然后借助三个指针,第一个指针指向当前值,第二个为当前值后一个数,第三个指针为最后一个数字
第二第三个指针不断缩小

var threeSum = function(nums) {
  if (nums.length < 3) {
    return [];
  }
  // 从小到大排序
  const arr = nums.sort((a,b) => a-b);
  // 最小值大于 0 或者 最大值小于 0,说明没有无效答案
  if (arr[0] > 0 || arr[arr.length - 1] < 0) {
    return [];
  }
  const n = arr.length;
  const res = [];
  for (let i = 0; i < n; i ++) {
    // 如果当前值大于 0,和右侧的值再怎么加也不会等于 0,所以直接退出
    if (nums[i] > 0) {
      return res;
    }
    // 当前循环的值和上次循环的一样,就跳过,避免重复值
    if (i > 0 && arr[i] === arr[i - 1]) {
      continue;
    }
    // 双指针
    let l = i + 1;
    let r = n - 1;
    while(l < r) {
      const temp = arr[i] + arr[l] + arr[r];
      if (temp > 0) {
        r --;
      }
      if (temp < 0) {
        l ++;
      }
      if (temp === 0) {
        res.push([nums[i], nums[l], nums[r]]);
        // 跳过重复值
        while(l < r && nums[l] === nums[l + 1]) {
          l ++;
        }
        // 同上
        while(l < r && nums[r] === nums[r - 1]) {
          r --;
        }
        l ++;
        r --;
      }
    }
  }
  return res;
};

总结

  • 在实际使用中对于求排序前几位或者动态插入元素的,可以优先考虑使用堆排序