算法:
基础:二分;分治;贪心;倍增
动态规划
排序:快排;归并;计数
搜索:回溯;递归
数据结构:
数组;字符串;链表;堆栈;二叉树
一. 二分法:
参考博客
时间复杂度按优劣排差不多集中在:
O(1), O(log n), O(n), O(n log n), O(n^2), O(n^k), O(2^n)
到目前位置,似乎我学到的算法中,时间复杂度是O(log n)
,好像就数二分查找法,其他的诸如排序算法都是 O(n log n)或者O(n2)。但是也正是因为有二分的 O(log n), 才让很多 O(n2)缩减到只要O(n log n)。
二分查找法主要是解决在“一堆数中找出指定的数”这类问题。
而想要应用二分查找法,这“一堆数”必须有一下特征:
存储在数组中
有序排列
所以如果是用链表存储的,就无法在其上应用二分查找法了。
至于是顺序递增排列还是递减排列,数组中是否存在相同的元素都不要紧。不过一般情况,我们还是希望并假设数组是递增排列,数组中的元素互不相同。
1. 二分查找法找目标元素
int bsearchWithoutRecursion(int array[], int low, int high, int target)
{
while(low <= high) // 注意这里有=,因为low=high的时候的那个元素,没有经过判断,也需要去判断一下
{
int mid = (low + high)/2;
if (array[mid] > target)
high = mid - 1; //因为那个元素已经比较过了,所以记得-1
else if (array[mid] < target)
low = mid + 1;
else //find the target
return mid;
}
// 数组中没有target
return -1;
}
注意:
- 循环条件:是
low <= high
注意等号=
- 循环里面是
mid-1
和mid+1
针对有重复元素的一些二分法的变形
参考
2. 查找第一个值等于给定值的元素
public int bsearch(int[] a, int n, int value) {
int low = 0;
int high = n - 1;
while (low <= high) {
int mid = low + ((high - low) >> 1);
if (a[mid] > value) {
high = mid - 1;
} else if (a[mid] < value) {
low = mid + 1;
} else {
if ((mid == 0) || (a[mid - 1] != value)) return mid;
else high = mid - 1;
}
}
return -1;
}
那当 a[mid]=value 的时候应该如何处理呢?
如果我们查找的是任意一个值等于给定值的元素,当 a[mid]
等于要查找的值时,a[mid]
就是我们要找的元素。但是,如果我们求解的是第一个值等于给定值的元素,当 a[mid]
等于要查找的值时,我们就需要确认一下这个 a[mid]
是不是第一个值等于给定值的元素。
我们重点看第 11 行代码。如果 mid 等于 0,那这个元素已经是数组的第一个元素,那它肯定是我们要找的;如果 mid 不等于 0,但 a[mid] 的前一个元素 a[mid-1] 不等于 value,那也说明 a[mid] 就是我们要找的第一个值等于给定值的元素。
如果经过检查之后发现 a[mid]
前面的一个元素 a[mid-1]
也等于 value,那说明此时的 a[mid] 肯定不是我们要查找的第一个值等于给定值的元素。那我们就更新 high=mid-1
,因为要找的元素肯定出现在 [low, mid-1]
之间。
3. 查找最后一个值等于给定值的元素
public int bsearch(int[] a, int n, int value) {
int low = 0;
int high = n - 1;
while (low <= high) {
int mid = low + ((high - low) >> 1);
if (a[mid] > value) {
high = mid - 1;
} else if (a[mid] < value) {
low = mid + 1;
} else {
if ((mid == n - 1) || (a[mid + 1] != value)) return mid;
else low = mid + 1;
}
}
return -1;
}
4. 查找第一个大于等于给定值的元素
在有序数组中,查找第一个大于等于给定值的元素。
public int bsearch(int[] a, int n, int value) {
int low = 0;
int high = n - 1;
while (low <= high) {
int mid = (low+high)/2;
if (a[mid] >= value) {
if ((mid == 0) || (a[mid - 1] < value)) return mid;
else high = mid - 1;
} else {
low = mid + 1;
}
}
return -1;
}
5. 查找第一个大于给定值的元素
public int bsearch(int[] a, int n, int value) {
int low = 0;
int high = n - 1;
while (low <= high) {
int mid = (low+high)/2;
if (a[mid] > value) { //要判断这个是不是第一个大于value的值
if ((mid == 0) || (a[mid - 1] <= value)) return mid;
else high = mid - 1;
} else {
low = mid + 1;
}
}
return -1;
}
6. 查找最后一个小于等于给定值的元素
public int bsearch7(int[] a, int n, int value) {
int low = 0;
int high = n - 1;
while (low <= high) {
int mid = (low+high)/2;
if (a[mid] > value) {
high = mid - 1;
} else {
if ((mid == n - 1) || (a[mid + 1] > value)) return mid;
else low = mid + 1;
}
}
return -1;
}
7. 查找最后一个小于给定值的元素
public int bsearch7(int[] a, int n, int value) {
int low = 0;
int high = n - 1;
while (low <= high) {
int mid = (low+high)/2;
if (a[mid] > value) {
high = mid - 1;
} else {
if ((mid == n - 1) || (a[mid + 1] > value)) return mid;
else low = mid + 1;
}
}
return -1;
}
二. 双指针法:
参考博客
双指针技巧再分为两类,一类是 「快慢指针」,一类是 「左右指针」。
前者解决主要解决链表中的问题,比如典型的判定链表中是否包含环;
后者主要解决数组(或者字符串)中的问题,比如二分查找。
1. 快慢指针的常见算法
判定链表中是否含有环
经典解法就是用两个指针,一个跑得快,一个跑得慢。如果不含有环,跑得快的那个指针最终会遇到 null,说明链表不含环;如果含有环,快指针最终会超慢指针一圈,和慢指针相遇,说明链表含有环。
boolean hasCycle(ListNode head) {
ListNode fast, slow;
fast = slow = head;
while (fast != null && fast.next != null) {
fast = fast.next.next;
slow = slow.next;
if (fast == slow) return true;
}
return false;
}
2. 左右指针的常用算法
两数之和。
给定一个升序排序数组,找到两个数使得他们的和为target。
只要数组有序,就应该想到双指针技巧。这道题的解法有点类似二分查找,通过调节 left 和 right 可以调整 sum 的大小:
int[] twoSum(int[] nums, int target) {
int left = 0, right = nums.length - 1;
while (left < right) {
int sum = nums[left] + nums[right];
if (sum == target) {
// 题目要求的索引是从 1 开始的
return new int[]{left + 1, right + 1};
} else if (sum < target) {
left++; // 让 sum 大一点
} else if (sum > target) {
right--; // 让 sum 小一点
}
}
return new int[]{-1, -1};
}