《算法筆記》總結No.7——二分(多例題詳解版)

一.二分查找

????????目前有一個有序數列,舉個例子,假設是1~1000,讓我們去查找931這個數字,淺顯且暴力的做法就是直接從頭到尾遍歷一遍,直到找到931為止。當n非常大,比如達到100w時,這是一個非常大的量級,考慮到效率的優劣這是不能接收的。

? ? ? ? 二分查找是基于有序序列的一種查找算法,所謂的有序是序列嚴格遞增:每次根據當前查找區間的中位數來判斷是否與目標相同,如果不同就根據當前大小以上個區間的中點作為區間的某一端,繼續執行這個二分查找過程~

? ? ? ? 高效的點在于,二分的每一步都可以去除區間中一半的元素,其時間復雜度是O(logn),學過數學的各位理科生都知道,對數的增加速度是非常慢的,甚至小于一次的冪函數,當N非常大的時候,越能體會到logN復雜度的含金量!

話不多說直接上代碼,依舊是博主自研的vector函數版:

#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;void BinarySearch(vector<int> V,int x){int left=0;int right=V.size()-1;      //數組的末位 int i=(left+right)/2;     //初始中點 int flag=0;// flag為0意味著未查詢到 while(flag==0&&left<=right){cout<<"當前查詢的元素下標為:"<<i<<endl;if(V[i]==x){cout<<i<<"位即為x的值~";flag=1;}else if(x>V[i])  //目標大于當前中點,中點的右一位+1作為新區間的左端點 {left=i+1;i=(left+right)/2;}else if(x<V[i])//目標小于當前中點,中點的左一位-1作為新區間的左端點{right=i-1;i=(left+right)/2;}					 }if(flag==0)cout<<"很遺憾,未找到~"<<endl;
} int main() {int n=0;vector<int> V;cout<<"請輸入數列規模:"<<endl; cin>>n;for(int i=1;i<=n;i++) //讀入數據 {int temp=0;cin>>temp;V.push_back(temp); }sort(V.begin(),V.end());//排序一下,保證V本身有序!//其實這種題目直接就是有序~int x=0;cout<<"請輸入待查詢的值:"<<endl;cin>>x;BinarySearch(V,x);return 0;
}

以書上的測試用例作為驗證組:

10

3 7 8 11 15 21 33 52 66 88?

11

首先給大家圖解畫一下過程,其實不難想象:

測試結果如下,沒什么bug:


????????tips:如果各位是考試或者寫什么數構的偽碼,其實用普通的數組就行,博主在實現的時候偏愛使用vector——這種隨時可以擴展的數組(向量)屬實給人極大的安全感,各位在閱讀的時候不要見怪~?


升級問題,假設序列中有多個相同的元素,請用二分法找到這個區間?(格式為左開右閉~)

其實和之前差不多,區別在于,我們要找到該元素第一次和最后一次出現的位置,因此之前的大小比較應該做出改善,如下:

  • 找左端點時,還是用mid來與目標值對比,如果mid值大于等于目標值,則說明目標值第一次出現的位置有可能還要往左,因此還要往左查詢,因此要令right=mid(向左縮小范圍~);如果mid比目標值小,則說明目標值第一次出現的位置還要再往右,應該令left=mid+1
  • 找右端點時,如果mid>x,說明最后的目標數x在mid處或者mid的左側,因此應該在left~mid里面繼續找,即right=mid;如果mid小于等于x,說明最后一個x一定在mid的右側,因此令left=mid+1
  • 其實兩者改變區間的方式一致,區別就在于改變的條件~

先是找左端點的函數:

int FindLeft(vector<int> V,int x){int left=0;int right=V.size()-1;      //數組的末位 int i=(left+right)/2;     //初始中點  while(left<right){//cout<<"當前查詢的元素下標為:"<<i<<endl;if(V[i]>=x)   //如果當前值大于等于目標值,說明最小的有可能是mid或者mid的左邊{right=i;i=(left+right)/2;}else if(V[i]<x){left=i+1;//如果當前值小于目標值,說明最小的還要再往右i=(left+right)/2;	}}return left; 
}

然后是找右端點的函數:

int FindRight(vector<int> V,int x){int left=0;int right=V.size()-1;      //數組的末位 int i=(left+right)/2;     //初始中點 while(left<right){//cout<<"當前查詢的元素下標為:"<<i<<endl;if(V[i]>x)   //如果當前值大于等于目標值,說明最大的有可能是mid或者mid的左邊{right=i;i=(left+right)/2;}else if(V[i]<=x){left=i+1;//如果當前值小于目標值,說明最大的可能還要再往右i=(left+right)/2;	}}return right; 
}

最后main函數調用:

int main() {int n=0;vector<int> V;cout<<"請輸入數列規模:"<<endl; cin>>n;for(int i=1;i<=n;i++) //讀入數據 {int temp=0;cin>>temp;V.push_back(temp); }sort(V.begin(),V.end());//排序一下,保證V本身有序!//其實這種題目直接就是有序~int x=0;cout<<"請輸入待查詢的值:"<<endl;cin>>x;cout<<x<<"的存在區間是:"<<endl;cout<<"["<<FindLeft(V,x)<<","<<FindRight(V,x)<<")"<<endl;return 0;
}

兩組測試用例:

  • 【1,2,2,2,3,3,3,3,3,4】——3
  • 【0,0,0,1,2,2,2,2,3,3】——2

沒什么bug~

tips:其實寫成雙閉區間也可以,一個道理~修改一下if條件即可


一些注意事項:

  • 在程序設計時,二分更多用的是非遞歸的設計方法
  • 當上界超越int范圍的一半時,計算中點的left+right這一算式可能會導致溢出,這里修改的策略是mid=left+(right-left)/2
  • 上面找左右端點的時候,需要將條件改為left<mid,不然會造成死循環

進一步思考上一步中尋找區間端點的過程:本質上,所有需要用到二分法的題目都是在解決:尋找有序序列中第一個滿足某條件元素的位置~?

????????如上,核心點就在于修改判斷的條件,這個條件,在序列中一定是從左到右先不滿足,然后滿足的過程~?

二.應用

1.方程根的近似值

先來看一個簡單的例子:求出根號n的近似值,也就是sqrt(n)。

與其用mid和sqrt(n)比,不如直接用mid平方和n比,再對mid開方,不難發現,這也是一個二分查找~

直接上代碼:

#include <iostream>
#include <cmath> 
using namespace std;void Calsqrt(int x)
{double n1=sqrt(x);cout<<x<<"的真實開根號值為:"<<n1<<endl;double left=trunc(n1),right=trunc(n1)+1;cout<<x<<"的取值范圍是:["<<left<<","<<right<<"]"<<endl;double i=0; while(right-left>0.0001)  //設置精度 {i=(left+right)/2;//初始中點 if((i*i)>x)  //目標大于待查找值,所以將上一個值作為右端點,查詢區間左移 right=i;	else //目標小于待查找值,所以將上一個值作為左端點,查詢區間右移left=i;}cout<<i<<endl;
}int main() {int n=0;cout<<"請輸入n的值:";cin>>n;Calsqrt(n);return 0;
}

注意點:

  • 與真正的二分查找相比,本次的二分其實是針對一個連續的函數因此所謂的left和right一定要為double型的,不然無效且死循環
  • 同樣的道理,在改變區間時,直接用left或者right與mid相等就行——考過考研數學一的道友肯定知道,對于連續性的函數,端點處劃分到哪個區間都不影響!?

2.裝水問題

如上,其實還是一個同樣的問題,關鍵要找到一個切入點:隨著水高度h的增加,面積S是不斷增加的——這符合二分要求序列遞增的前提!

因此我們可以對h進行二分,并計算當前h下的面積與目標面積的差距,然后還是左右移動二分區間的套路,即可解決~

#include <iostream>
#include <cmath> 
using namespace std;#define Pi 3.14 void CalH(double R,double r)
{double sq1=R*R*Pi;//原始面積double sq2=sq1*r;//目標面積double answer=sq2/Pi;//設置一個中間值為目標h的平方倍 double i=0; //二分中點double left=0,right=R;// 二分區間 while(right-left>0.0001&&r<=1)  //設置精度 ,比例不能超過1 {i=(left+right)/2;//初始中點 if((i*i)>answer)  //目標大于待查找值,所以將上一個值作為右端點,查詢區間左移 right=i;	else //目標小于待查找值,所以將上一個值作為左端點,查詢區間右移left=i;}cout<<"目標h的值為:"<<i<<endl;
}int main() {double R=0,r=0;cout<<"請輸入半徑的值:"<<endl;cin>>R;cout<<"請輸入比例的值:"<<endl;cin>>r;CalH(R,r);return 0;
}

這里我們選一個測試用例檢測一下:

沒什么問題~

?

3.木棒切割問題

還是老套路:找到遞增和目標值,兩個條件直接選用二分!

不難發現,當每一根木棒的長度越大時,分出來的個數肯定越小,因此應該用當前單體長度來進行二分,從小到大——當對應木棒的個數不符合題意中要求的個數時,則結束二分。

直接上代碼:

#include <iostream>
#include <vector>
#include <cmath> 
using namespace std;int Cut(vector<int>V,int k) //當前長度最多在數組中切多少~ 
{int count=0;for(int i=0;i<=V.size()-1;i++){int temp=0;temp=V[i]/k;count+=temp;	}	return count;
} void CalN(vector<int> V,int k)
{int min=V[0];for(int i=1;i<=V.size()-1;i++)if(V[i]<min)min=V[i];//找出最小值作為二分區間的右端點 int left=1,right=min;//最短也要長度為1,最長也超不過單體的最短~int i=0; //二分中點while(right-left>1)  //當差距是一位時,說明已經找到了,由于向下取整的原因會造成死循環,因此此時可以直接輸出中點的值 {i=(left+right)/2;//初始化中點if(Cut(V,i)>=k) //如果當前長度下的段數比目標的多,說明每一段的長度還可以再長,因此區間右移 	left=i;else          //如果當前的比目標的還少,應該縮短每一段的長度來增加個數,使得滿足要求~ right=i;		 	 }cout<<"最大長度為:"<<i<<endl; }int main() {int num=0;vector<int> V;cout<<"請輸入木棒的個數:"<<endl;cin>>num;cout<<"請輸入每段木棒的長度:"<<endl;for(int i=1;i<=num;i++){int temp=0;cin>>temp;V.push_back(temp);}int K=0;cout<<"請輸要求長度相等的木棒個數:"<<endl;cin>>K;CalN(V,K); return 0;}

測試就用書上的測試兩次:

和手算的結果一致,沒什么問題:

三.快速冪

????????快速冪,顧名思義,一種快速計算冪函數的方法。博主的個人理解是,這玩意既像二分的思想,又像遞歸的思想,因此也可以稱之為二分冪~

現有數2的10000000次方,我們當然不能把2累乘10000000次——這是相當失敗的程序。其實中間有很多步驟可以省略:

  • 當指數是偶數時,我們可以讓指數除以2,底數乘以底數;
  • 當指數是奇數時,我們可以將指數變為偶數;

舉個例子,2的10次方,使用快速冪的思想,計算過程如下:

  • 2的10次方,10是偶數,此時待算式為:4的5次
  • 5為奇數,因此待算式為:4*4的4次
  • 4為偶數,因此待算式為:4*16的2次
  • 2為偶數,因此待算式為:4*16*256的一次
  • 1為奇數,因此待算式為:4*16*256*256的0次,直接計算出來~

不難發現,上述過程只需要5步,而累乘需要10步,當指數變得非常大時,這一優勢會愈加明顯~

代碼如下:

1.非遞歸形態

#include<iostream>
using namespace std;
long long fpow(long long a,long long b){//a是底數,b是指數 long long ans=1;//初始化答案為1while(b){//當指數不為0時執行if(b%2==0){//指數為偶數時,指數除以2,底數乘以底數b/=2;a*=a; }else{//指數為奇數時,分離指數,ans乘以底數ans*=a; b--;}} return ans; 
}
int main(){long long n,m;cin>>n>>m;cout<<fpow(n,m)<<endl;
}

2.遞歸形態

#include<iostream>
using namespace std;
typedef long long LL;LL binaryPow(LL a,LL b)
{if(b==0) 	//遞歸邊界——即0次方 return 1;    if(b%2==1)   //奇偶各一次遞歸式 return a*binaryPow(a,b-1);else{LL mul =binaryPow(a,b/2);return mul*mul;}
}int main(){long long n,m;cin>>n>>m;cout<<binaryPow(n,m)<<endl;
}

tips:寫在最后

  • 對于二分法的使用時機,主要要看兩個點:需要找到某個數值第一次或者最后一次出現的位置且在尋找這個值的過程中,滿足該值單調遞增(遞減)
  • 二分的循環條件,基本上都是right>left,或者作差大于某個值~
  • 文中基本上全是博主根據偽碼自創的實現方式,博主酷愛使用vector,可能有些朋友看起來比較吃力;此外所有的mid都寫成了i,大家看的時候盡量理解~

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/45733.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/45733.shtml
英文地址,請注明出處:http://en.pswp.cn/web/45733.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Linux 線程初步解析

1.線程概念 在一個程序里的一個執行路線就叫做線程&#xff08;thread&#xff09;。更準確的定義是&#xff1a;線程是“一個進程內部的控制序列。在linux中&#xff0c;由于線程和進程都具有id,都需要調度等等相似性&#xff0c;因此都可以用PCB來描述和控制,線程含有PCB&am…

美聯儲降息應該更早?高盛:有充分理由7月降息,而非9月

KlipC報道&#xff1a;高盛首席經濟學家哈祖斯Jan Hatzius表示&#xff0c;美聯儲“有充分理由”在7月會議上降息&#xff0c;而非等到9月。 在最新發布的報告中&#xff0c;他表明通脹已經取得了足夠的進展&#xff0c;回到了美聯儲2%的長期目標附近&#xff0c;這將使美聯儲…

[C++ 入門基礎 - 命名空間]

在C中&#xff0c;命名空間&#xff08;Namespace&#xff09;是一種用來組織代碼并避免命名沖突的機制。命名空間可以包含變量、函數、類等C中的所有實體&#xff0c;使得這些實體的名稱在命名空間內部有效&#xff0c;避免了與其他命名空間或全局作用域中相同名稱的沖突。 文…

實現將Nginx的每個網站配置單獨的訪問日志

一、問題描述 Nginx默認的訪問日志是不會區分哪個網站有哪些日志的,全部糅雜在一起;如果需要哪個網站有哪些訪問日志記錄,還需要將訪問日志下載下來后篩選,比較麻煩;希望將每個網站對應的日志能夠單獨記錄到對應的日志文件里面,方便排查和管理。 # 進入Nginx默認的日志文…

為什么Vim是程序員最喜歡的編輯器之一

簡介 Vim&#xff0c;全稱Vi IMproved&#xff0c;是一種高度可定制、功能強大的文本編輯器。自其誕生以來&#xff0c;它以高效、快速和靈活的特點深受程序員喜愛。無論是處理簡單的文本文件還是復雜的代碼項目&#xff0c;Vim都能提供卓越的編輯體驗。許多資深程序員甚至稱其…

c++ primer plus 第16章string 類和標準模板庫,6.1.5字符串種類

c primer plus 第16章string 類和標準模板庫,6.1.5字符串種類 c primer plus 第16章string 類和標準模板庫,6.1.5字符串種類 文章目錄 c primer plus 第16章string 類和標準模板庫,6.1.5字符串種類6.1.5字符串種類 6.1.5字符串種類 本節將 string 類看作是基于 char 類型的。…

web服務器經過代理后的絕對路徑問題,以及 dirname(__FILE__)和__DIR__

web服務器經過代理后的絕對路徑問題&#xff0c;以及 dirname&#xff08;__FILE__&#xff09;和__DIR__ 問題描述情況解析資源路徑分析訪問過程分析 dirname(\_\_FILE\_\_) 與 \_\_DIR\_\_ 同步發布在個人筆記web服務器經過代理后的絕對路徑問題&#xff0c;以及 dirname(__F…

Nest.js 實戰 (一):使用過濾器優雅地統一處理響應體

前言 在我們實際的業務開發中&#xff0c;我們可以看到后端接口返回格式都有一定的要求&#xff0c;假如我們統一規定接口的統一返回格式為&#xff1a; {data: any; // 業務數據code: number; // 狀態碼msg: string; // 響應信息timestamp: number; // 時間戳 }那么在 Nest.…

【智能算法改進】改進的麻雀搜索算法及其求解旅行商問題

目錄 1.算法原理2.改進點3.結果展示4.參考文獻5.代碼獲取 1.算法原理 【智能算法】麻雀搜索算法&#xff08;SSA&#xff09;原理及實現 2.改進點 改進發現者更新位置 為了使 SSA 算法能夠避開向原點收斂的弊端, 將算法向最優位置跳躍的操作轉換為向最優位置的移動: X i ,…

自己動手寫一個滑動驗證碼組件(后端為Spring Boot項目)

近期參加的項目&#xff0c;主管丟給我一個任務&#xff0c;說要支持滑動驗證碼。我身為50歲的軟件攻城師&#xff0c;當時正背著雙手&#xff0c;好像一個受訓的保安似的&#xff0c;中規中矩地參加每日站會&#xff0c;心想滑動驗證碼在今時今日已經是標配了&#xff0c;司空…

一個篇文章告訴你一個APP前端搭建有多簡單

用uni-app 1.新建uni-app項目 點擊項目 2.創建 最后點擊右下方創建 3.添加tarbar 首先你要創建幾個頁面這里比如說我有兩個頁面的tarbar首頁(home)和我的(userIndex) 在pages目錄下右鍵新建頁面即可

從庫存超賣問題分析鎖和分布式鎖的應用(二)

本文從一個經典的庫存超賣問題分析說明常見鎖的應用&#xff0c;假設庫存資源存儲在Redis里面。 假設我們的減庫存代碼如下&#xff1a; Autowired StringRedisTemplate redisTemplate;public void deduct(){String stock redisTemplate.opsForValue().get("stock"…

JavaSE從零開始到精通

1.前置知識 JVM&#xff1a;java virtrual machine, java虛擬機, 專門用于執行java代碼的一款軟件。JRE&#xff1a;java runtime enviroment, java運行時環境, java官方提供的核心類庫. jre中包含了核心類庫和jvm。JDK: java development kit, java開發工具包, javac.exe, ja…

LVS+Keepalive高可用

1、keepalive 調度器的高可用 vip地址主備之間的切換&#xff0c;主在工作時&#xff0c;vip地址只在主上&#xff0c;vip漂移到備服務器。 在主備的優先級不變的情況下&#xff0c;主恢復工作&#xff0c;vip會飄回到住服務器 1、配優先級 2、配置vip和真實服務器 3、主…

我想做信號通路分析,但我就是不想學編程

“我想做信號通路分析&#xff0c;但我就是不想學編程。” “我又不是生信狗&#xff0c;學代碼會死。” “你們這些做生信的&#xff0c;整天把數據分析搞得神神秘秘&#xff0c;不就是怕被人搶飯碗而已嘛。” “這都沒分析出我想要的結果&#xff0c;不靠譜。” “你們做…

【自學安全防御】二、防火墻NAT智能選路綜合實驗

任務要求&#xff1a; &#xff08;銜接上一個實驗所以從第七點開始&#xff0c;但與上一個實驗關系不大&#xff09; 7&#xff0c;辦公區設備可以通過電信鏈路和移動鏈路上網(多對多的NAT&#xff0c;并且需要保留一個公網IP不能用來轉換) 8&#xff0c;分公司設備可以通過總…

使用Docker創建并運行一個create-react-app應用(超簡單)

創建并運行一個使用 Create React App (CRA) 創建的應用程序的 Docker 容器涉及幾個步驟。以下是一個詳細的過程&#xff0c;包括創建一個簡單的 React 應用、編寫 Dockerfile、構建鏡像以及運行容器。 步驟 1: 創建一個新的 React 應用 如果你還沒有一個 React 應用&#xf…

Java爬蟲安全策略:防止TikTok音頻抓取過程中的請求被攔截

摘要 在當今互聯網時代&#xff0c;數據采集已成為獲取信息的重要手段。然而&#xff0c;隨著反爬蟲技術的不斷進步&#xff0c;爬蟲開發者面臨著越來越多的挑戰。本文將探討Java爬蟲在抓取TikTok音頻時的安全策略&#xff0c;包括如何防止請求被攔截&#xff0c;以及如何提高…

RK3568 安卓12 EC20模塊NOCONN沒有ip的問題(已解決)

從網上東拼西湊找了不少教程&#xff0c;但是里面沒有提到rillib.so需要替換&#xff0c;替換掉就可以上網了&#xff0c;系統也有4G圖標了。 注意&#xff0c;這個rillib.so是移遠提供的。把他們提供的文件放到rk3568_android_sdk/vendor/rockchip/common/phone/lib下&#x…

Andriod Stdio新建Kotlin的Jetpack Compose簡單項目

1.選擇 No Activity 2.選擇kotlin 4.右鍵選擇 在目錄MyApplication下 New->Compose->Empty Project 出現下面的畫面 Finish 完成