# 数组
数组是组成其他数据结构的基础结构之一(另外一个是链表)。数组是一段连续的空间,不可动态扩容(Vec是进行了封装)。数组和链表的异同在于:
- 都是线性存储结构
- 数组连续,链表不连续
- 数组读取操作更快,链表插入、删除操作更快
基于数组的算法设计也有很多(滑动窗口、二分搜索、双指针技巧等)。
# 二分搜索
二分搜索用于有序表的搜索,可以提升搜索效率,主要思路是对半缩小搜索区间来查找元素。算法思路可以用伪代码来表示:
function binarySearch(nums, target) {
let left = 0
let right = nums.length - 1 || 其他条件确定的边界值
while(搜索条件) {
let mid = left + (right - left) / 2
if (nums[mid] === target) {
执行搜索到结果的逻辑
} else if (nums[mid] < target) {
left = new_left // 在右半区间,left需要以某种逻辑去更新
} else if (nums[mid] > target) {
right = new_right // 在左搬区间,right需要以某种逻辑去更新
}
}
return 结果
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 最基础的二分搜索,查找一个数
function binarySearch(nums, target) {
let left = 0
let right = nums.length - 1 // ⚠️1
while (left <= right) { // ⚠️2
let mid = left + Math.floor((right - left) / 2)
if (nums[mid] === target) {
return mid
} else if (nums[mid] > target) {
right = mid - 1 // ⚠️3
} else if (nums[mid] < target) {
left = mid + 1 // ⚠️4
}
}
return -1
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
有几个需要注意的地方:
right
被赋值为数组的最后一个索引,这代表区间[left, right]
是左右都闭合的。- 由于左右都闭合,所以当
left === right
时,代表此时还剩最后一个元素需要被检查。 - 当前区间过大时,
right
被赋值为mid - 1
而不是mid
,这也是由于[left, right]
区间左右都闭合,因此需要将mid
从下一次查找中剔除 - 跟第三点一样。
前两条主要针对于while
条件的结束,后两条针对于左右区间的边界构造。这两点都需要根据具体的情况来决定,看你的搜索区间是[left, right)
、[left, right]
、(left, right]
、(left, right)
中的哪一种(一般不会考虑左开的写法)。
# 寻找左侧边界
有序列表中可能存在多个target,找出处于最左侧的元素。
function leftBound(nums, target) {
if (!nums.length) return -1
let left = 0
let right = nums.length // ⚠️1
while(left < right) { // ⚠️2
let mid = left + Math.floor((right - left) / 2)
if (nums[mid] === target) {
right = mid
} else if (nums[mid] > target) {
right = mid // ⚠️3
} else if (nums[mid] < target) {
left = mid + 1
}
}
if (left >= nums.length || nums[left] !== target) return -1
return left
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
这里有三个地方跟之前的写法不同,主要还是区间问题导致的:
right
被赋值为nums
的长度,此时nums[right]
是越界的,因此右侧区间是开区间。- 由于区间右开,这就导致当
left === right
时,区间内是没有元素的。 - 不需要赋值为
mid - 1
,因为右区间是开区间
最后还有基于结果值、越界的判断等。
# 寻找右侧边界
同上一个问题思路相同,只是需要向右区间逼近,所以需要做一些修改
...
while(left < right) {
...
if (nums[mid] === target) {
left = mid + 1
}
}
if (left === 0) return -1
nums[left - 1] === target ? left - 1 : -1
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
这里返回的值是left - 1
,因为左区间是闭合的,当条件不符合时,左区间被多余移动了一步,因此需要做一下回退。
# 滑动窗口
滑动窗口相关可以看这里滑动窗口
# 数组删除相关
数组由于其连续性和不可动态扩容,在尾部增删时还是比较方便的,但是如果在中间进行增删,就比较麻烦了,因为需要对数据进行搬移,填补空白。这里主要说一种基于快慢指针的方法。
# 有序数组、有序链表去重
给定一个有序数组,去除重复的元素
数组
function removeDuplicates(nums) {
if (!nums.length) return 0;
let slow = 0
let fast = 0
while(fast < nums.length) {
if (nums[fast] != nums[slow]) {
slow += 1
nums[slow] = nums[fast]
}
fast += 1
}
return slow + 1
}
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
链表
function removeDuplicates(head) {
if (!head) return null
let slow = head
let fast = head
while(fast) {
if (slow.val !== fast.val) {
slow.next = fast
slow = slow.next
}
fast = fast.next
}
slow.next = null
return head
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
数组和链表两种结构的去重思路基本一致
# 删除数组中的指定元素
function removeElement(nums, val) {
let slow = 0
let fast = 0
while(fast < nums.length) {
if (nums[fast] !== nums[slow]) {
nums[slow] = nums[fast]
slow += 1
}
fast += 1
}
return slow
}
1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
# 双指针
双指针可以分为两类:快慢指针和左右指针。
其中快慢指针在链表中的应用比较多,例如:
- 判断链表是否有环
- 找到链表环入口
- 寻找链表中点
- 寻找倒数第n个节点
- ...
左右指针在数组中应用比较多,例如:
- 逆转数组
- 二分搜索
- 滑动窗口
- ...
可以到具体笔记中查看这些算法更加细致的表述。