六、二分查詢與二叉查詢樹(小象)
二分查詢演算法(遞迴,迴圈)
具有分治思想的多用迴圈,具有回溯思想的多用遞迴。
二分或者二叉排序樹都是在 分治的解決問題
二分查詢:
二分查詢:待查數是跟中間的數對比,只有查詢的數恰好等於中間的數返回正確;
遞迴
若比中間的數大,則去搜索右區間 [mid +1 ,end ]
若比中間的數小,則去搜索左區間 [begin ,mid - 1]
函式引數傳引用的原因是:
拷貝大的類型別物件或者容器物件比較低效,所以通過引用形參訪問該型別的物件。
bool binary_search(vector<int> &vec,int begin ,int end ,int search_num) { if (begin > end) { return false; } int mid = ( end + begin ) / 2; if (vec[mid] == search_num) { return true; } else if (search_num > vec[mid]) { binary_search(vec, mid+1 , end, search_num); } else if (search_num < vec[mid]) { binary_search(vec, begin, mid-1 , search_num); } }
迴圈:
bool binary_search(vector<int> &vec, int search_num) { int begin = 0; int end = vec.size() - 1; /*只有在搜查範圍合法的時候才有查詢的必要*/ while (begin <= end) { int mid = (end + begin) / 2; if (vec[mid] == search_num) { return true; } if (search_num > vec[mid]) { begin = mid + 1; } else if (search_num < vec[mid]) { end = mid - 1; } } return false; }
總結:
分清楚迴圈和遞迴的兩個查詢條件,一個是begin<= end 的時候才繼續查詢,一個是begin > end 的時候才停止遞迴搜尋。
35.搜尋插入的位置
邊界條件mid==0時,此時還不等於search_value就說明要查詢的那個值比最前面的元素都要小,說明只能插入到第一個元素的位置(0);同理,mid== nums.size()-1,只能插入到最後。
class Solution { public: int searchInsert(vector<int>& nums, int target) { int index = -1; int begin = 0; int end = nums.size()-1; while(index== -1){ int mid = (begin+end)/2; if(nums[mid] ==target ){ index = mid; } else if(target > nums[mid]){ if( mid == nums.size()-1 || target < nums[mid+1]){ index = mid+1; } begin = mid +1 ; } else if(target < nums[mid]){ if(mid == 0 || target > nums[mid-1]){ index = mid; } end = mid -1 ; } } return index; } };
34. 在排序陣列中查詢元素的第一個和最後一個位置
思考:還是基礎的二分查詢,只是在判斷左右邊界的時候要新增約束條件。
左邊界:若查詢到的mid等於target,並且陣列的前一個小於target或者mid已經是陣列的最前面的元素,則這個元素的位置就是左邊界;否則說明此時查詢到的mid並不是左邊第一個,再搜尋區間[begin,mid-1],然後找到第一個出現的。
右邊界:若查詢到的mid等於target,並且陣列的後一個大於target或者mid已經是陣列的最後面的元素,則這個元素的位置就是右邊界;否則說明此時查詢到的mid並不是右邊最後一個,再搜尋區間[mid+1,end],然後找到最後一個出現的。
int left_bound(std::vector<int>& nums, int target) {
int begin = 0;
int end = nums.size()-1;
while (begin <= end ) {
int mid = (begin + end) / 2;
if (nums[mid] == target) {
if (nums[mid - 1] < target || mid == 0) {
return mid;
}
//若nums[mid - 1] == target 則說明此時查詢到的mid並不是左邊第一個
//再搜尋區間[begin,mid-1],然後找到第一個出現的
end = mid - 1;
}
else if (target >nums[mid]) {
begin = mid + 1;
}
else if (target <nums[mid]) {
end = mid - 1;
}
}
return -1;
}
int right_bound(std::vector<int>& nums, int target) {
int begin = 0;
int end = nums.size() - 1;
while (begin <= end) {
int mid = (begin + end) / 2;
if (nums[mid] == target) {
if (nums[mid + 1] > target || mid == nums.size() - 1) {
return mid;
}
//若nums[mid + 1] == target 則說明此時查詢到的mid並不是右邊最後一個
//再搜尋區間[mid+ 1 ,end],然後找到第一個出現的
begin = mid + 1;
}
else if (target >nums[mid]) {
begin = mid + 1;
}
else if (target <nums[mid]) {
end = mid - 1;
}
}
return -1;
}
class Solution {
public:
vector<int> searchRange(vector<int>& nums, int target) {
vector<int> range;
range.push_back( left_bound( nums, target));
range.push_back( right_bound( nums, target));
return range;
}
};
33、搜尋旋轉排序陣列
思考:
旋轉了之後,整個陣列不再滿足升序的關係,如果此時查詢一個數,單純的通過和nums[mid]進行對比,可能會判斷錯誤查詢的區間,從而得到錯誤的解。比如,要查詢圖中的“0”元素,nums[mid] = 7,則會到左邊查詢,可是此時陣列的兩部分,左邊部分[4,5,6],並沒有0,所以會返回查詢失敗。若查詢圖中“5”元素,則可以通過二分查詢查到。就是說在二分查詢的時候,我們要新增一些約束條件,才能到正確的查詢區間去尋找target。
特徵:給定的升序陣列,無論怎麼旋轉,它的nums[begin]一定滿足大於nums[end]。因為它是繞著某個點旋轉,前面的部分叫做part1,後面叫part2,則part2的第一個結點一定大於part1的最後一個結點,所以nums[begin]一定滿足大於nums[end]。
演算法思路:
class Solution {
public:
int search(vector<int>& nums, int target) {
int begin = 0;
int end = nums.size()-1;
while(begin <= end){
int mid= (begin + end)/2;
//整體還是滿足二分查詢,只是在查詢的時候添加了一些約束條件。
if(nums[mid]==target){
return mid;
}
//因為nums[begin] > nums[end]
if(target > nums[mid]){//找旋轉區間和遞增區間
if(nums[begin] < nums[mid]){
//則說明是遞增的區間
//但是此時target > nums[mid],就肯定不會在這個區間了,所以去另外一個區間查詢
begin = mid + 1;
}
if(nums[begin] > nums[mid]){
//則說明是旋轉區間
//但是,並不能一定判斷它就在旋轉區間,也有可能在遞增區間
if(target >= nums[begin]){
//若taget比nums[begin]還大,又因為nums[begin] > nums[end],此時肯定不會在遞增區間了,所以在旋轉區間搜尋。
end = mid -1;
}
else{
//否則就只用在遞增區間搜尋
begin = mid +1;
}
if(nums[mid]== nums[begin]){
//此時就兩個數了,肯定只用查詢剩下的那個了
begin = mid +1 ;
}
}
}
if(target < nums[mid]){
//遞增區間
if(nums[begin] < nums[mid]){
if(target >= nums[begin]){
end = mid -1 ;
}
else{
begin = mid +1 ;
}
}
//旋轉區間
else if(nums[begin] > nums[mid]){
end = mid -1;
}
else if(nums[begin] == nums[mid]){
begin = mid +1 ;
}
}
}
return -1;
}
};
二叉查詢樹:
1、二叉查詢(排序)樹的定義
2、二叉查詢樹插入結點(遞迴)
程式碼:
void BST_insert(TreeNode* root, TreeNode* node) {
if (node->value < root->value) {
if (!root->left) {
root->left = node;
}
else {
BST_insert(root->left, node);
}
}
else {
if (!root->right) {
root->right = node;
}
else {
BST_insert(root->right, node);
}
}
}
3、二叉查詢樹搜尋
bool BST_search(TreeNode* root, TreeNode* node) {
if (node->value == root->value) {
return true;
}
if (node->value < root->value) {
if (root->left) {
BST_search(root->left, node);
}
else {
return false;
}
}
else {
if (root->right) {
BST_search(root->right, node);
}
else {
return false;
}
}
}
程式碼:
bool BST_search(TreeNode* root, TreeNode* node) {
if (node->value == root->value) {
return true;
}
if (node->value < root->value) {
if (root->left) {
BST_search(root->left, node);
}
else {
return false;
}
}
else {
if (root->right) {
BST_search(root->right, node);
}
else {
return false;
}
}
}
449.序列化與反序列化二叉搜尋樹(二叉查詢樹的編碼與解碼)
為什麼要實現序列化和反序列化?
在計算機網路傳輸的過程中,如果有一個數據結構要傳遞給另一個主機,不能直接拷貝這個資料結構的程式碼,所以得通過把它儲存成字串的形式,然後到另一個主機通過解碼把它還原成原來的結構。
先序遍歷:8 3 1 6 10 15 //按照這種重新插入成一個新的二叉搜尋樹和原先的是一樣的,所以按照先序遍歷得到的結果,再按照二叉查詢樹插入,可以得到原始的二叉查詢樹。
中序遍歷:1 3 6 8 10 15 //首先是二叉樹查詢樹的根節點都變了,並且,這樣插入成了一個右斜的樹,成了個連結串列了。
後序遍歷:1 6 3 10 15 8 //二叉樹查詢樹的根節點都變了
整型轉字串(字串轉整型)
1、整型轉字串
把某一個數轉換成字串的形式。
比如89,要從int --->> string ,首先要定義個字串str,再利用對10取餘,獲得最低位的9,此時9是int型,加一個字元'0',然後就變成了字元型的'9',此時把數字89除以10,獲得次低位8,迭代停止的條件就是while(num),最後再對字串取反,這裡用的是<algorithm>中的reverse;也可以利用for(size_t i = str.size();i >= 0;i++),逆序一下。最後新增的“#”,代表一個終止符,標記一下的意思,因為如果你傳入一串數字,再解碼的時候,不知道從哪裡斷開是不行的。
程式碼:
string IntegerTostring(int num, string &temp) {
while (num) {
temp += num % 10 + '0';
num /= 10;
}
reverse(temp.begin(), temp.end());
temp += "#";
return temp;
}
2、字串轉整型
比如字串“89#23#”,從string--->> int,首先要定義一個整數integer,對字串每一位檢索,當遇到"#"的時候,則輸出一個數字,同時把臨時變數integer清零,以便接下來還要用它來輸出第二個數字;如果是檢索到字元的時候,integer = integer * 10 + str[i] - '0';因為是從高到低的遍歷,所以一開始讀的是高位,要乘以10。
程式碼:
int stringToInteger(string str, int &integer) {
for (auto _str : str) {
if (_str == '#') {
return integer;
}
else {
//integer += _str * 10 - '0';
integer += integer * 10 + _str - '0';
}
}
return integer;
}
整體的程式碼:
/**
* Definition for a binary tree node.
* struct TreeNode {
* int val;
* TreeNode *left;
* TreeNode *right;
* TreeNode(int x) : val(x), left(NULL), right(NULL) {}
* };
*/
void integerToString(int num, string &str) {
while (num) {
str += num % 10 + '0';
num /= 10;
}
reverse(str.begin(), str.end());
str += "#";
}
void stringToInteger(string &str, vector<TreeNode*>& node_num) {
int integer = 0;
for (auto item : str) {
if (item == '#') {
node_num.push_back(new TreeNode(integer));
integer = 0;/*每一個數字儲存了之後要把integer清空*/
}
else {
integer = integer * 10 + item - '0';
}
}
}
void preOrder(TreeNode* root,vector<int>& node_num ) {
if (!root) {
return;
}
node_num.push_back(root->val);
preOrder(root->left, node_num);
preOrder(root->right, node_num);
}
void BST_insert(TreeNode* root, TreeNode * node) {
if (root->val < node->val) {
if (root->right) {
BST_insert(root->right, node);
}
else {
root->right = node;
}
}
else {
if (root->left) {
BST_insert(root->left, node);
}
else {
root->left = node;
}
}
}
class Codec {
public:
// Encodes a tree to a single string.
string serialize(TreeNode* root) {
//先序遍歷
vector<int> node_val;
string str;
if(!root){
return str;
}
preOrder(root, node_val);
for (auto item : node_val) {
string temp;
integerToString(item, temp);
str += temp;
}
return str;
}
// Decodes your encoded data to tree.
TreeNode* deserialize(string data) {
if(data.empty()){
return NULL;
}
vector<TreeNode*> node_ptr;
stringToInteger(data, node_ptr);
for (size_t i = 1; i < node_ptr.size(); i++) {
BST_insert(node_ptr[0], node_ptr[i]); /*直接把第一個節點當做根節點。*/
}
return node_ptr[0];
}
};
// Your Codec object will be instantiated and called as such:
// Codec codec;
// codec.deserialize(codec.serialize(root));
315.計算右側小於當前元素的個數
思考:
這個題上次用的歸併排序和pair<>對來解決的,這次利用二叉搜尋樹來解決。
利用的資料結構:多出一個count;
struct BST_node {
int val;
int count; /*用來儲存左子樹數量*/
BST_node* left;
BST_node* right;
BST_node(int x) :val(x), count(0), left(NULL), right(NULL) {}
};
!!!關鍵!!!
為什麼count_small += node->count + 1 ?
答:以陣列[1,-2,5,3,1]為例,如果這個時候要插入的是9。首先9大於root->val(1),所以直接肯定大於root左側的2個,此時它的count_small += root->count + 1 /*加1 在於比這個root也更大了啊!*/ ,然後插入到右子樹,這時遇到了root->val(5),它的左子樹個數是1個,然後又插入到5的右側,此時count_small = 3 + 2= 5;正確!如果只是單純的count_small = root->count + 1,那麼多次更新之後9 的count_small 反而只等於2,錯誤!
遇到的問題:這裡用size_t 定義的i,可以看出來!i是無符號數!!!所以i = 0 的時候,再減1 ,反而成了個無窮大的數!哎!
程式碼:
struct Treenode{
int val;
int count;
Treenode* left;
Treenode* right;
Treenode(int x):val(x),count(0),left(NULL),right(NULL){}
};
void BST_insert(Treenode* root,Treenode* node,int &smaller_count){
if(node->val > root->val){
smaller_count += root->count + 1 ;
if(root->right){
BST_insert(root->right,node,smaller_count);
}
else{
root->right = node;
}
}
else{
root->count++;
if(root->left){
BST_insert(root->left,node,smaller_count);
}
else{
root->left = node;
}
}
}
class Solution {
public:
vector<int> countSmaller(vector<int>& nums) {
vector<Treenode*> node_vec;
vector<int> count;
if(nums.empty()){
return count ;
}
for(int i = nums.size() - 1 ;i >= 0 ;i--){
node_vec.push_back(new Treenode(nums[i]));
}
count.push_back(0);
for(size_t i = 1; i<node_vec.size();i++ ){
int smaller_count = 0;
BST_insert(node_vec[0],node_vec[i],smaller_count);
count.push_back(smaller_count);
}
reverse(count.begin(),count.end());
return count;
}
};
最大的體會是今天不走,明天要跑。