1月5日更新
1月4日训练 二分+双指针
提示:本文主要记录下自己学习算法时候的想法捏😋
训练的第一天,在家摆烂一个月天天看番打游戏捏,一个多月了鼠鼠我啊,回来学习了捏😊
一、尺取法(双指针)
算法介绍(双指针与二分查找)
双指针算法优化了二分查找的算法,二分查找的时间复杂度为 O ( log 2 n ) O(\log_2n) O(log2n)
- 二分查找的时间复杂度的解释:在一个序列中,每次进行一次比较都要去掉一半的数组元素,假设数组内有 n n n个元素( n n n为 2 2 2的幂),第一次比较剩下了 n / 2 n/2 n/2个元素,第二次比较剩下 ( n / 2 ) / 2 (n/2)/2 (n/2)/2个元素,以此类推,经过第k次比较之后,需要查找的元素就剩下了 n / 2 k n/2^k n/2k个,当 k = log 2 k k=\log_2k k=log2k时,需要查找的元素只剩下一个了,就只需要在比较一次,因此在最最糟糕的情况下,也只需要 log 2 n + 1 \log_2n+1 log2n+1次查找捏👍
- 双指针算法的时间复杂度解释:两个指针,一个指向头部,一个指向尾部,向中间靠拢,直到找到我们需要的元素,所以最糟糕的情况下,设数组有 n n n个元素,这俩指针只能沿着一个方向,把这个数组扫一遍(最糟糕的情况下)所以时间复杂度为 O ( n ) O(n) O(n)
下面是一个经典的双指针算法例子
//比如说在一个数组a中我们要朝朝两个数使他们的和为target
void find(int a[] , int n , int target){
sort(a,a+n);
int i = 0 , j = n - 1;
while(i < j ){
if(a[i] + a[j] > target) j--;//这说明太大了,所以代表大元素的尾指针后退一步
else if(a[i] + a[j] < target) i++;//同理
else cout<<a[i]<<' '<<a[j];
使用双指针创造滑动区间寻找区间和
void findsum(int *a , int len , int target){
int i , j = 0;//初始化指针
int sum = a[0];//初始化和为头指针指向的下标元素
while(j < len){
if(sum >= target){
if(sum == target) print("%d %d\n",i,j);//找到一个合适的区间,输出下标
sum -=a[i];//头指针后移,先减掉头指针后移之前指向的元素
i++;
if(i > j){
//如果i超越了j(通常是第一个元素就大于target值所以头指针需要后移导致超越了尾指针)
sum = a[i];//重置这个和为最新的头指针所指向的元素(准确的说直到找到一个合适的数据,否则头指针和尾指针都是并一块走的)
j++;//尾指针后移
}else{
j++;
sum+=a[j];//尾指针后移,加上新的元素
利用双指针算法进行数组去重
void delsame(int *a,int len){//本算法有缺陷,因为我暂时没有想到一个普世的处理办法,所以只能对每个数组的最后一个元素进行一个特殊判断,但我也觉得是一个双指针算法;
int i ,j ,k;
int cmp[10010];//这个数组保存去重之后的数组
for(i = 0 , j = 0 , k = 0 ;i < n ; i++){//i指针用于遍历数组,j指针用于指向需要保存的元素,k指针是新数组cmp的下标
if(a[i] != a[j]){//如果不相同 就把j指针的值保存到cmp数组里
cmp[k++] = a[j];
j = i;//因为此时i与j的值不相同所以直接把i赋值给j
}
if( i == n - 1 && cmp[k] != a[i])//判断最后扫描的值之前是否有保存过,如果保存了,就不需要录进去了,没保存我们就录入
cmp[k++] = a[i];
}
二、二分法(折半)
整数二分(细节很重要,牵扯到了一些比较底层的东西)
int bin_search(int *a ,int left , int right , int target){//查找后继
int i = left , int j = right;
while(i < j){
int mid = i + (j - i)/2;
if(a[mid] >= target) j = mid;
else left = mid + 1;
//以上是半开半闭写法,如果喜欢全闭区间只需要修改j和返回的mid值即可
int bin_search1(int *a,int left , int right , int target){//查找前驱
int i = left , int right = right ;
while(i < j){
int mid = i + ( j - i + 1) /2;
if(a[mid] <= target) i = mid;
else j = mid - 1;
public static int FindPoinner(int b[], int len ,int target) {
int i = 0 , j = len;
while(i < j) {
int mid = i + (j - i + 1)/2;//向下取整,这个是右中位数
if(b[mid] <= target) {
i = mid ;
}else {
j = mid - 1;
}
}
return i ;
}
public static int FindEnd(int b[],int len ,int target) {
int i = 0 , j = len;
while(i < j) {
int mid = i + (j - i)/2;//向上取整,这个是左中位数
if(b[mid] >= target) {
j = mid;
}else {
i = mid + 1;
}
}
return i ;
}
- m i d mid mid的计算是向下取整,更加靠近左边,所以是左中位数,也就是说在 a [ m i d ] > = t a r g e t a[mid]>=target a[mid]>=target的情况下最后返回的 m i d mid mid值是第一个等于或第一个大于 t a r g e t target target的下标,也成为 t a r g e t target target的后继。
- 尽量不要使用 m i d = ( i + j ) / 2 mid=(i+j)/2 mid=(i+j)/2这种写法,因为这是向零取整,虽然在C++语言里头不用担心但是在Java,Python这种有负数下标存在的语言里会导致死循环
- 同样的如果要使用右中位数,就需要 m i d mid mid向上取整, m i d = i + ( j − i + 1 ) / 2 mid=i+(j-i+1)/2 mid=i+(j−i+1)/2那么这样算法返回的值就是最后一个等于或者最后一个大于 t a r g e t target target的下标,也成为 t a r g e t target target 的前驱
- 注意:在查找前驱时,如果要查找的数为有序序列的最大值,则会出现越界一位的现象
STL中的lower_bound()与upper_bound()
- 二者功能是一样的,如果只是简单的查找x或者x附近的数,那么用 S T L STL STL就行
//查找第一个大于x的元素的位置
pos = upper_bound(a,a+n,x) - a;
//查找第一个等于或者大于x的元素
lower_bound();
//以此为例可以进行很多变式写法
实数二分
- 没有取整的问题所以代码相对二分简单
const double eps = 1e-7
while(right - left > eps){
double mid = left + (right - left)/2;
if(check(mid)) right = mid;
else left = mid;
重点在于仔细设计精度eps,太小会超时,太大会不准确