二分查找
二分查找的优势
- 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
};
题目
搜索插入位置
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的平方根 常考
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
};
寻找峰值
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
}
}
};
排序
快速排序
参考
快速排序:通过一趟排序将要排序的数据分割成独立的两部分,其中一部分的所有数据比另一部分的所有数据要小,再按这种方法对这两部分数据分别进行快速排序,整个排序过程可以递归进行,使整个数据变成有序序列。思想类似于二分查找,逐步缩小排序空间。
时间复杂度:平均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;
}
插入排序
插入排序适合大部分已经被排好序的数据,每次往后取一个未排序的数字,插入到前面已排序的数组中。
时间复杂度: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)的一个非常典型的应用。(分治法将问题分成一些小的问题然后递归求解,而治的阶段则将分的阶段得到的各答案"修补"在一起,即分而治之)。
步骤
- 将序列中带排序数字分为若干组,每个数字分为一组
- 将若干个组两两合并,保证合并后的组是有序的(两两合并时比较两个组的头部谁更小,小的那个移到合并的数组里,当其中一个组没有值时,另一个组剩余的数放在合并数组的后面)
- 重复第二步操作直到只剩下一组,排序完成
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;
}
堆排序
对于一个堆来说,要符合以下两个特点
- 是一个完全二叉树
- 所以父节点的值都要大于(或小于)子节点的值
完全二叉树的性质
- 如果父元素为i, 则左子树为2i,右子树为2i+1。下标从零开始(jsi + 1 = i),左子树为2i+1,右子树为2i+2。
- 第n层的节点数最多为2^n个节点, 从0开始算层
- n层二叉树最多有
2^n+1 - 1个节点 - 第一个非叶子节点: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个数
题目
小顶堆
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
};
合并两个有序数组
思路:利用归并排序的思路,比较两数组第一个值,但是这里由于需要存储在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--]
}
}
};
多数元素
法一:排序后找中间的数,因为出现次数大于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
};
有效的字母异位词
var isAnagram = function(s, t) {
return s.length === t.length && [...s].sort().join('') === [...t].sort().join('')
};
三数之和
思路:
这道题最容易想到的是三重循环,而三重循环的时间复杂度太高,可以进一步优化,利用排序+双指针优化到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;
};
总结
- 在实际使用中对于求排序前几位或者动态插入元素的,可以优先考虑使用堆排序