前面两篇讲的排序,熟悉算法应用的童鞋都知道,排序其实是很多算法的子过程,也就是说很多算法在执行自己前,都要先对数据进行排序,才能发挥算法的核心优势。所以排序说完了,下面就要说算法的一个核心应用,搜索。
一、二分查找法
二分查找法的思想非常朴实和经典,可以想象一样比如我们猜别人的年龄,一开始肯定是根据他的长相外貌特征,说一个大概的数字,如果对方说不对,那我们一定会追问对方是大了还是小了,而说出来的第一个数字其实就是二分查找法中使用的中点。
使用二分查找法有个前提,必须是有序数组才能使用二分查找法。
用图来表示的话很简单:
每次找到数组的中点v,看看是否等于我们要找的数值,如果等于则完成任务,否则小于则在v左边继续执行二分查找,反之则在右边。
上代码:
template<typename Item>
int binarySearch(Item arr[], int length, Item target)
{
int l = 0;
int r = length - 1;
while(l <= r)
{
int mid = l + (r - l) / 2;
if(target == arr[mid])
{
return mid;
}
if(target > arr[mid])
{
l = mid + 1;
}
else
{
r = mid - 1;
}
}
return -1;
}
注意看mid取值的时候没有使用(l + r) / 2,为的是防止数据溢出。
二、二分搜索树
二分搜索树使用了字典的数据存储方式,也就是我们常用的keyValue。操作的时候只要操作key,然后根据key拿出其中的value就可以了。
二分搜索树是一颗二分树,但不是完全二分树,它有以下几个特性:
每个节点的键值大于左叶子;
每个节点的键值小于右叶子;
以左右叶子为根节点的树仍然是二分搜索树;
因为二分搜索树不是完全二分树,所以较难用数组来进行存储,这里必须用到链表的结构。
一个典型的二分搜索树长这样:
先定义一个二分搜索树节点的结构体:
struct Node
{
Key key;
Value value;
Node* left;
Node* right;
Node(Key key, Value value)
{
this->key = key;
this->value = value;
this->left = this->right = NULL;
}
Node(Node* otherNode)
{
this->key = otherNode->key;
this->value = otherNode->value;
this->left = otherNode->left;
this->right = otherNode->right;
}
};
下面来讲二分搜索树的几个核心操作的代码,这里用到了大量的递归,大家看代码的时候可以体会到递归在这里的应用是多么的巧妙:
1.插入节点
让当前需要插入的元素e,与根节点的元素r进行对比,如果e小于r,则让e与r的左叶子为根节点的树继续进行对比,如此递归;如果e等于r,这里执行的更新当前节点的操作,当然根据不同的场景也可以进行其他操作,这里默认是更新;如果e大于r,则让e与r的右叶子为根节点的树进行对比,如此递归。
Node* insert(Node* node, Key key, Value value)
{
if(node == NULL)
{
count++;
return new Node(key, value);
}
if(key == node->key)
{
node->value = value;
}
else if(key < node->key)
{
node->left = insert(node->left, key, value);
}
else
{
node->right = insert(node->right, key, value);
}
return node;
}
希望大家好好递归在这里运用;
2.查找节点
根据二分搜索树的性质很容易想到查找节点的方法,让当前需要查找的元素e,与根节点的元素r进行对比,如果e == r,那就已经找到;如果e < r,则就到以r的左叶子为树的二分搜索树中去递归查找;同理e > r,则就到以r的右叶子为树的二分搜索树中去递归查找。
bool contain(Node* node, Key key)
{
if(node == NULL)
{
return false;
}
if(key == node->key)
{
return true;
}
else if(key < node->key)
{
return contain(node->left, key);
}
else
{
return contain(node->right, key);
}
}
Value* search(Node* node, Key key)
{
if(node == NULL)
{
return NULL;
}
if(key == node->key)
{
return &(node->value);
}
else if(key < node->key)
{
return search(node->left, key);
}
else
{
return search(node->right, key);
}
}
这里写了两个方法,一个判断数据是否存在,一个则是真正的搜索,返回key对应的value指针。理解了原理的话,用递归来写算法真的是很简单。
3.深度优先遍历
这三种遍历是典型的代码比解释好写的…… 直接上代码
void preOrder(Node* node)
{
if(node != NULL)
{
cout << node->key << ", ";
preOrder(node->left);
preOrder(node->right);
}
}
void inOrder(Node* node)
{
if(node != NULL)
{
inOrder(node->left);
cout << node->key << ", ";
inOrder(node->right);
}
}
void postOrder(Node* node)
{
if(node != NULL)
{
postOrder(node->left);
postOrder(node->right);
cout << node->key << ", ";
}
}
void destroy(Node* node)
{
if(node != NULL)
{
destroy(node->left);
destroy(node->right);
delete node;
count--;
}
}
大家可以看到,代码真的很简单,只是cout的位置不一样而已,cout这句代码是可以换成对当前节点的其他任意复杂操作的。注意看最后一个destroy方法,其实一个二分搜索树的类在析构内部的Node链表的时候,用的就是后序遍历。
4.广度优先遍历
简单来说就是一层一层地来看二分搜索树上的数据。在代码编写的时候需要用到辅助的数据结构:队列。
void levelOrder()
{
queue<Node*> q;
q.push(root);
while(!q.empty())
{
Node* node = q.front();
q.pop();
cout << node->key <<", ";
if(node->left)
{
q.push(node->left);
}
if(node->right)
{
q.push(node->right);
}
}
}
每次把根节点的数据放入队列,然后出队,再遍历它的左右节点,分别入队,如此递归循环。这样就能一层一层的拿出二分搜索树中的数据。
5.获取最小和最大节点
获取最小节点的方法很简单,根据二分搜索树的特性,只要一直顺着左叶子递归找,直到找到一个不再有左叶子的节点为止。
最大节点就是反过来去右叶子找。
Node* getMin(Node* node)
{
if(node->left == NULL)
{
return node;
}
return getMin(node->left);
}
Node* getMax(Node* node)
{
if(node->right == NULL)
{
return node;
}
return getMax(node->right);
}
6.删除最小、最大节点
学会了找到最小最大节点之后,删除这两种节点也是很容易的:
最小节点,拿到该节点的右叶子(因为最小节点不可能有左叶子),并把右叶子的指针取代原节点指针就行。
最大节点反之亦然。
Node* removeMin(Node* node)
{
if(node->left == NULL)
{
Node* right = node->right;
count--;
delete node;
return right;
}
node->left = removeMin(node->left);
return node;
}
Node* removeMax(Node* node)
{
if(node->right == NULL)
{
Node* left = node->left;
count--;
delete node;
return left;
}
node->right = removeMax(node->right);
return node;
}
7.删除任意节点
任意节点删除和6所述的删除最大的区别就是会存在一种情况,被删除的节点左叶子右叶子都存在。根据二分搜索树的特点,我们只要用当前节点右叶子中的最小节点或者左叶子中的最大节点来替换掉当前被删除节点就可以了。
Node* remove(Node* node, Key key)
{
if(node == NULL)
{
return NULL;
}
if(key < node->key)
{
node->left = remove(node->left, key);
return node;
}
else if(key > node->key)
{
node->right = remove(node->right, key);
return node;
}
//key == node->key
else
{
if(node->left == NULL)
{
Node* right = node->right;
delete node;
count--;
return right;
}
else if(node->right == NULL)
{
Node* left = node->left;
delete node;
count--;
return left;
}
//node->left != NULL && node->right != NULL
else
{
Node* s = new Node(getMin(node->right));
count++;
s->left = node->left;
s->right = removeMin(node->right);
delete node;
count--;
return s;
}
}
}
最后上一下完整的二分搜索树的代码:
//
// BST.h
// IndividualTestCpp
//
// Created by Amuro on 2017/5/5.
// Copyright © 2017 Amuro. All rights reserved.
//
#ifndef BST_h
#define BST_h
#include <iostream>
#include <string>
#include <queue>
using namespace std;
template <typename Key, typename Value>
class BST
{
private:
struct Node
{
Key key;
Value value;
Node* left;
Node* right;
Node(Key key, Value value)
{
this->key = key;
this->value = value;
this->left = this->right = NULL;
}
Node(Node* otherNode)
{
this->key = otherNode->key;
this->value = otherNode->value;
this->left = otherNode->left;
this->right = otherNode->right;
}
};
Node* root;
int count;
Node* insert(Node* node, Key key, Value value)
{
if(node == NULL)
{
count++;
return new Node(key, value);
}
if(key == node->key)
{
node->value = value;
}
else if(key < node->key)
{
node->left = insert(node->left, key, value);
}
else
{
node->right = insert(node->right, key, value);
}
return node;
}
bool contain(Node* node, Key key)
{
if(node == NULL)
{
return false;
}
if(key == node->key)
{
return true;
}
else if(key < node->key)
{
return contain(node->left, key);
}
else
{
return contain(node->right, key);
}
}
Value* search(Node* node, Key key)
{
if(node == NULL)
{
return NULL;
}
if(key == node->key)
{
return &(node->value);
}
else if(key < node->key)
{
return search(node->left, key);
}
else
{
return search(node->right, key);
}
}
void preOrder(Node* node)
{
if(node != NULL)
{
cout << node->key << ", ";
preOrder(node->left);
preOrder(node->right);
}
}
void inOrder(Node* node)
{
if(node != NULL)
{
inOrder(node->left);
cout << node->key << ", ";
inOrder(node->right);
}
}
void postOrder(Node* node)
{
if(node != NULL)
{
postOrder(node->left);
postOrder(node->right);
cout << node->key << ", ";
}
}
void destroy(Node* node)
{
if(node != NULL)
{
destroy(node->left);
destroy(node->right);
delete node;
count--;
}
}
Node* getMin(Node* node)
{
if(node->left == NULL)
{
return node;
}
return getMin(node->left);
}
Node* getMax(Node* node)
{
if(node->right == NULL)
{
return node;
}
return getMax(node->right);
}
Node* removeMin(Node* node)
{
if(node->left == NULL)
{
Node* right = node->right;
count--;
delete node;
return right;
}
node->left = removeMin(node->left);
return node;
}
Node* removeMax(Node* node)
{
if(node->right == NULL)
{
Node* left = node->left;
count--;
delete node;
return left;
}
node->right = removeMax(node->right);
return node;
}
Node* remove(Node* node, Key key)
{
if(node == NULL)
{
return NULL;
}
if(key < node->key)
{
node->left = remove(node->left, key);
return node;
}
else if(key > node->key)
{
node->right = remove(node->right, key);
return node;
}
//key == node->key
else
{
if(node->left == NULL)
{
Node* right = node->right;
delete node;
count--;
return right;
}
else if(node->right == NULL)
{
Node* left = node->left;
delete node;
count--;
return left;
}
//node->left != NULL && node->right != NULL
else
{
Node* s = new Node(getMin(node->right));
count++;
s->left = node->left;
s->right = removeMin(node->right);
delete node;
count--;
return s;
}
}
}
public:
BST()
{
root = NULL;
count = 0;
}
int size()
{
return count;
}
bool isEmpty()
{
return count == 0;
}
void insert(Key key, Value value)
{
this->root = insert(root, key, value);
}
bool contain(Key key)
{
return contain(root, key);
}
Value* search(Key key)
{
return search(root, key);
}
void preOrder()
{
preOrder(root);
}
void inOrder()
{
inOrder(root);
}
void postOrder()
{
postOrder(root);
}
void levelOrder()
{
queue<Node*> q;
q.push(root);
while(!q.empty())
{
Node* node = q.front();
q.pop();
cout << node->key <<", ";
if(node->left)
{
q.push(node->left);
}
if(node->right)
{
q.push(node->right);
}
}
}
Key getMin()
{
if(count == 0)
return NULL;
return getMin(root)->key;
}
Key getMax()
{
if(count == 0)
return NULL;
return getMax(root)->key;
}
void removeMin()
{
if(count == 0)
return;
root = removeMin(root);
}
void removeMax()
{
if(count == 0)
return;
root = removeMax(root);
}
void remove(Key key)
{
root = remove(root, key);
}
~BST()
{
destroy(root);
}
};
void testBST()
{
BST<int, string> bst = BST<int, string>();
bst.insert(10, "a");
bst.insert(8, "b");
bst.insert(2, "c");
bst.insert(7, "d");
bst.insert(1, "e");
bst.insert(13, "f");
bst.insert(4, "g");
bst.insert(6, "h");
bst.preOrder();
cout << endl;
bst.inOrder();
cout << endl;
bst.postOrder();
cout << endl;
bst.levelOrder();
cout << endl;
cout << bst.getMin() << ", " << bst.getMax() << endl << endl;
cout << "do delete" << endl;
bst.removeMin();
bst.removeMin();
bst.removeMax();
bst.levelOrder();
}
#endif /* BST_h */
对比下二分搜索树和其他数据结构的搜索效率,明显是优于普通数组的。
另外,二分搜索树有个重大的缺陷,就是如果按照1,2,3,4这样的顺序插入数据时,得到的二分搜索树是这样的:
没错,退化成了链表,想要解决这个问题,需要用到更复杂的平衡二叉树,常见的就是红黑树,有兴趣的同学可以继续深入研究。