二叉树
二叉树的周游
二叉树结点的抽象数据类型:
template<class T>
class BTreeNode
{
friend class BTree<T>;//声明二叉树类为结点类的友元类,以便访问私有数据
private:
T info;//二叉树结点数据域
public:
BTreeNode(); //默认构造函数
BTreeNode(const T& ele);//给定数据的构造函数
BTreeNode(const T& ele, BTreeNode<T>* l, BTreeNode<T>* r);//子树构造结点
T value() const; //返回当前结点的数据
BTreeNode<T>* lchild()const;//返回当前结点的左子树
BTreeNode<T>* rchild()const;//返回当前结点的右子树
void setLchild(BTreeNode<T>*);//设置当前结点的左子树
void setRchild(BTreeNode<T>*);//设置当前结点的右子树
void setValue(const T& val);//设置当前结点的数据域
bool isLeaf()const; //判断当前结点是否为叶结点
BTreeNode<T>& operator=(const BTreeNode<T>& Node);//重载赋值操作符
};
二叉树的抽象数据类型
template<class T>
class BTree {
private:
BTreeNode<T>* root; //二叉树根节点
public:
BTree() { root = NULL; }//构造函数
~BTree() { DeleteBTree(root); }//析构函数
bool isEmpty()const;//判断二叉树是否为空树
BTreeNode<T>* Root() { return root; };//返回二叉树根结点
BTreeNode<T>* Parent(BTreeNode<T>* current);//返回当前结点的父结点
BTreeNode<T>* Lsibling(BTreeNode<T>* current);//返回当前结点的左兄弟
BTreeNode<T>* Rsibling(BTreeNode<T>* current);//返回当前结点的右兄弟
void CreatTree(const T& info, BTree<T>& Ltree, BTree<T>& Rtree);//构造新树
void PreOrder(BTreeNode<T>* root);//前序周游给定二叉树
void InOrder(BTreeNode<T>* root);//中序周游给定二叉树
void PostOrder(BTreeNode<T>* root);//后序周游给定二叉树
void LevelOrder(BTreeNode<T>* root);//按层次周游给定二叉树
void DeleteBTree(BTreeNode<T>* root);//删除给定的二叉树
};
- 用递归实现二叉树的深度优先周游
深度优先周游二叉树或其子树
template <class T>
void BTree<T>::PreOrder(BTreeNode<T>* root) {
//前序周游二叉树或其子树
if (root != NULL) {
Visit(root->value());//访问当前结点
PreOrder(root->lchild());//前序周游左子树
PreOrder(root->rchild());//前序周游右子树
}
}
template<class T>
void BTree<T>::InOrder(BTreeNode<T>* root) {
//中序周游二叉树或其子树
if (root != NULL) {
InOrder(root->lchild());//中序周游左子树
Visit(root->value());//访问当前结点
InOrder(root->rchild());//中序周游右子树
}
}
template<class T>
void BTree<T>::PostOrder(BTreeNode<T>* root) {
//后序周游二叉树或其子树
if (root != NULL) {
PostOrder(root->lchild());//后序周游左子树
PostOrder(root->rchild());//后序周游右子树
Visit(root->value());//访问当前结点
}
}
-
深度优先周游二叉树的非递归算法
如何把递归程序转化成等价的非递归算法?解决这个问题的关键就是设置一个栈结构。按照递归算法执行过程中编译栈的工作原理,可以写出非递归周游二叉树的算法。
非递归前序周游算法的主要思想是:每遇到一个结点,先访问该结点,并把该结点的非空右子节点推入栈中,然后周游其左子树;左子树周游不下去时,从栈顶弹出待访问的结点,继续周游。 算法执行过程中,只有非空结点入栈。为了算法简洁,最开始压入一个空指针作为监视哨;当这个空指针被弹出来时,则周游结束。
非递归前序周游二叉树或其子树
template<class T>
void BTree<T>::PreOrderWithoutRecursion(BTreeNode<T>* root){
using std::stack; //使用STL中的栈
stack<BTreeNode<T>*>aStack;
BTreeNode<T>* pointer = root;
aStack.push(NULL);//栈底监视哨
while(pointer) //或者!aStack.empty()
{
Visit(pointer->value());//访问当前结点
if (pointer->rchild() != NULL)
aStack.push(pointer->rchild());
if (pointer->lchild() != NULL)
pointer = pointer->lchild();//左路下降
else {
//左子树访问完毕,转向访问右子树
pointer = aStack.top();//获得栈顶元素
aStack.pop(); //栈顶元素退栈
}
}
}
非递归中序周游二叉树算法的主要思想是:每遇到一个结点就把它压入栈,然后去周游其左子树;周游完左子树后,从栈顶弹出并访问这个结点,然后按照其右链接指示的地址再去周游该结点的右子树
非递归中序周游二叉树的非递归算法
template<class T>
void BTree<T>::InOrderWithoutRecursion(BTreeNode<T>* root) {
using std::stack;//使用STL中的栈
stack<BTreeNode<T>*>aStack;
BTreeNode<T>* pointer = root;
while (!aStack.empty() || pointer) {
if (pointer) {
aStack.push(pointer);//当前指针入栈
pointer = pointer->lchild();//左路下降
}
else {
//左子树访问完毕,转向访问右子树
pointer = aStack.top();//获得栈顶元素
aStack.pop();//栈顶元素退栈
Visit(pointer->value());//访问当前结点
pointer = pointer->rchild();//指针指向右孩子
}
}
}
后序周游二叉树时最先处理左子树,然后是右子树,最后才访问当前结点。
在非递归的后序周游算法中,先把它压入栈中,去周游它的左子树;周游完它的左子树后,应继续周游该结点的右子树;周游完右子树之后,才从栈顶弹出该结点并访问它。由于访问某个节点前需要知道是否已经访问该结点的右子树,因此需要给栈中每个元素加一个标志位tag。标志位用枚举类型Tags表示:Left表示已进入该结点的左子树;Right表示已进入该结点的右子树。
enum Tags { Left, Right };//定义枚举类型标志位
template<class T>
class StackElement {
//栈元素的定义
public:
BTreeNode<T>* pointer;//指向二叉树结点的指针
Tags tag;//标志位
};
template<class T>
void BTree<T>::PostOrderWithoutRecursion(BTreeNode<T>* root) {
using std::stack;//使用STL的栈
StackElement<T>element;
stack<StackElement<T>>aStack;
BTreeNode<T>* pointer;
if (root == NULL)//如果是空树则返回
return;
else pointer = root;
while (!aStack.empty() || pointer) {
while (pointer != NULL) {
//如果当前指针非空则压栈并下降到最左子节点
element.pointer = pointer;
element.tag = Left;//置标志位为Left,表示进入左子树
aStack.push(element);
pointer = pointer->lchild();
}element = aStack.top();//获得栈顶元素
aStack.pop();//栈顶元素退栈
pointer = element.pointer;
if (element.tag == Left) {
//如果从左子树返回
element.tag = Right;//置标志位为Right,表示进入右子树
aStack.push(element);
pointer = pointer->rchild();
}
else {
//如果从右子树返回
Visit(pointer->value());//访问当前结点
pointer = NULL;//置point指针为空,以继续弹栈
}
}
}
不管采用哪种周游方式,对于有n个结点的二叉树,周游完树的所有元素都需要O(n)时间。只要对每个结点的处理(函数Visit的执行)时间是一个常数,那么,周游二叉树就可以在线性时间内完成。所需要的辅助空间为周游过程中栈的最大容量,即树的高度。最坏情况下,具有n个结点的二叉树高度为n,所需要的空间复杂度为O(n)。
广度优先周游二叉树
根据层次周游二叉树的性质,这里需要使用一个队列作为辅助的存储结构。层次周游过程的实现就是从根结点开始逐层逐个地访问各个节点。
**在周游开始的时候,首先将根结点放入队列;然后每次从队列中取出队头元素进行处理,每处理一个结点时,按从左至右的顺序把它的所有子节点放入队列。这样,上层结点总是排在下一层结点的前面,从而实现了二叉树的广度优先周游**
利用队列实现广度优先周游二叉树。初始化时,根结点插入到空队列中,周游的每一步,算法都将从队列头上删除一个结点,并将其子结点插入队尾。
template<class T>
void BTree<T>::LevelOrder(BinaryTreeNode<T>* root) {
void BTree<T>::LevelOrder(BTreeNode<T> * root) {
usint std::queue; //使用STL的队列
queue<BTreeNode<T>*>aQueue;
BTreeNode<T>* pointer = root;
if (pointer)
aQueue.push(pointer);//根节点入队列
while (!aQueue.empty()) {
//队列非空
pointer = aQueue.front();//获得队列首节点
Visit(pointer->value());//访问当前结点
if (pointer->lchild() != NULL)
aQueue.push(pointer->lchild());//左子树进队列
if (pointer->rchild() != NULL)
aQueue.push(pointer->rchild());//右子树进队列
}
}
}
广度优先周有一颗具有n个结点的二叉树,其时间复杂度也是O(n)。队列所需要的最大存储空间由二叉树中具有最多结点数目的那一层上的结点个数来决定。因此,周游一科满的完全二叉树所需要的队列空间最大,最大长度为(n+1)/2.
二叉树的存储结构
二叉树的链式存储结构
template<class T>
bool BTree<T>::isEmpty()const {
//判定二叉树是否为空树
return (root != NULL ? false : true);
}
template<class T>
BTreeNode<T>* BTree<T>::Parent(BTreeNode<T>* current) {
using std::stack;//使用STL中的栈
stack<BTreeNode<T>*>aStack;
BTreeNode<T>* pointer = root;
if (root != NULL && current != NULL) {
while (!aStack.empty() || pointer) {
if (pointer != NULL) {
if (current == pointer->lchild() || current == pointer->rchild())
return pointer;//如果pointer的孩子是current则返回parent
aStack.push(pointer);//当前指针入栈
pointer = pointer->lchild();//当前指针指向左孩子
}
else {
//左子树访问完毕,访问右子树
pointer = aStack.top();//获得栈顶元素
aStack.pop();//栈顶元素退栈
pointer = pointer->rchild();//当前指针指向右孩子
}
}
}
}
//创建一棵新树,参数info为根结点元素,lTree和rTree是不同的两棵树
template<class T>
void BTree<T>::CreateTree(const T& info, BTree<T>& lTree, BTree<T>& rTree) {
root = new BTreeNode<T>(info, lTree.root, rTree.root);//创建新树
lTree.root = rTree.root = NULL;//原来两棵子树的根结点置为空,避免非法访问
}
template<class T>
void BTree<T>::DeleteBTree(BTreeNode<T>* root) {
//后序周游删除二叉树
if (root != NULL) {
DeleteBTree(root->left);//递归删除左子树
DeleteBTree(root->right);//递归删除右子树
delete root;
}
}
二叉搜索树
二叉搜索树(binary search tree,BST,也可称为二叉排序树、二叉查找树等)。
二叉搜索树的性质:
- 二叉搜索树中的每个非空结点表示一个记录;
- 若某结点的左子树不为空,则左子树上所有结点的值均小于该结点的关键码值。
- 若其右子树为空,则右子树上所有结点的值均大于该结点的关键码值;
- 二叉搜索树也可以是一棵空树,任何节点的左右子树都是二叉搜索树。
- 按照中序周游整个二叉树可得到一个由小到大的有序排列。
二叉搜索树的检索:
假设要在二叉搜索树中检索关键码key,则从根结点开始,
如果根结点存储的值为key,则返回检索结果,检索结束。
如果不是,则必须检索树的更深层。
将给定值key与根结点的关键码比较,如果key小于根结点的值,则只需要检索左子树;如果key大于根结点的值,则只检索右子树。
这个过程一直持续到key被匹配成功或者遇到叶结点为止。如果遇到叶结点仍没有发现key,则说明key不在这棵二叉搜索树中。
二叉搜索树的插入算法
二叉搜索树插入操作:
将待插入节点的关键码与根结点的关键码相比较,若待插入的关键码小于根结点的关键码,则进入左子树,否则进入右子树。
按照同样的方式沿检索路径直到叶结点,确定插入位置,把待插入结点作为一个新叶结点插入到二叉搜索树中。
template<class T>
void BSTree<T>::InsertNode(BTreeNode<T>* root, BTreeNode<T>* newpointer) {
//root指向二叉搜索树的根,newpointer指向待插入的新结点
BTreeNode<T>* pointer = NULL;
if (root == NULL) {
//如果是空树
Initialize(newpointer);//则用指针newpointer作为树根
return;
}
else pointer = root;
while (pointer != NULL) {
if (newpointer->value() == pointer->value())//如果存在相等的元素则不用插入
return;
else if (newpointer->value() < pointer->value()) {
//如果待插入结点小于pointer的关键码值
if (pointer->lchild() == NULL) {
//如果pointer没有左孩子
pointer->left = newpointer;//newpointer作为pointer的左子树
return;
}
else pointer = pointer->leftchild();//向左下降
}
else {
//若待插入结点大于pointer的关键码值
if (pointer->rchild() == NULL)//如果pointer没有右孩子
{
pointer->right = newpointer;//newpointer作为pointer的右子树
return;
}
else pointer = pointer->rchild();//向右下降
}
}
}
二叉搜索树结点的删除
要保持二叉搜索树的性质,就不能在二叉搜索树中留下一个空位置,因此需要用另一个结点来填充这个位置并且保持性质。
设pointer、temppointer是指针变量,其中pointer表示要删除的结点。首先,找到待删除的结点pointer,删除该结点的过程如下:
- 若结点pointer没有左子树,则用pointer右子树的根代替被删除的结点pointer;
- 若pointer有左子树,则在左子树里找到按中序周游的最后一个结点temppointer,把temppointer的右指针设置成指向pointer的左子树的根,然后用结点pointer左子树的根代替被删除的结点pointer。
改进的二叉搜索树结点删除算法的思想:
- 若结点pointer没有左子树,则用pointer1右子树的根代替被删除的结点pointer。
- 若结点pointer有左子树,则在左子树中按中序周游的最后一个结点temppointer(即左子树中的最大结点)并将其从二叉搜索树中删除。由于temppointer没有右子树,因此删除该结点只需用temppointer的左子树代替temppointer,然后用temppointer结点代替待删除的结点pointer。
//改进的二叉搜索树的结点删除
template<class T>
void BSTree<T>::DeleteNodeEx(BTreeNode<T>* pointer) {
if (pointer == NULL)//若待删除结点不存在则返回
return;
BTreeNode<T>* temppointer;//用于保存替换结点
BTreeNode<T>* tempparent = NULL;//用于保存替换结点的父结点
BTreeNode<T>* parent = Parent(pointer);//用于保存待删除结点的父结点
if (pointer->lchild() == NULL)//如果待删除结点的左子树为空
temppointer = pointer->rchild();//替换结点赋值为其右子树的根
else {
//如果待删除结点左子树不空,在左子树中寻找最大结点作为替换结点
temppointer = pointer->lchild();
while (temppointer->rchild() != NULL) {
//寻找左子树中的最大结点
tempparent = temppointer;
temppointer = temppointer->rchild();//向右下降
}
if (tempparent == NULL)//如果替换结点就是被删结点的左子结点
pointer->left = temppointer->lchild();//替换结点左子树挂接到被删结点的左子树
else tempparent->right = temppointer->lchild();//替换结点的左子树作为其父结点右子树
temppointer->left = pointer->lchild();//继承pointer的左子树
temppointer->right = pointer->rchild();//继承pointer的右子树
}
//下面用替换结点代替待删除结点
if (parent == NULL)
root = temppointer;
else if (parent->lchild() == pointer)
parent->left = temppointer;
else parent->right = temppointer;
delete pointer;//删除该结点
pointer = NULL;
return;
}
堆与优先队列
堆的定义及其实现
**最小堆(min-heap,最小值堆)是关键码序列{K0,K1…K(n-1)},**它具有如下特性:
Ki≤K(2i+1),
Ki≤K(2i+2)(i=0,1,… ,)
比较复杂的堆操作是插入和删除;在此先考虑插入操作的实现。
首先,新添的元素加入末尾。为了保持最小堆的性质,需要沿着其祖先的路径,自下而上依次比较和交换该结点与父结点的位置,直到重新满足堆的性质为止。
在插入的过程中总是自下而上逐渐上升,最后停在满足最小堆性质的位置,这个过程通常被称为“筛选”。
删除操作的处理与插入时方向相反。考虑到删除某个位置的元素后形成了一个空的位置,首先把最末端的结点填入这个位置。同理,这样做也可能导致破坏最小堆的堆序特性。末端元素需要与被删位置的子结点比较交换,知道过滤到该结点小于最小子结点的正确位置为止。
建堆:
首先,将所有关键码放到一维数组中,此时形成的完全二叉树并不具备最小堆的特性,但是仅包含叶子结点的子树已经是堆,即在有n个结点的完全二叉树中,当i>(n/2)(向下取整)-1时,以关键码Ki为根的子树已经是堆。这时,从含有内部结点数最少的子树开始,从右至左依次进行调整。对这一层调整完成后,继续对上一层进行同样的工作,直到整个过程到达树根时,整棵完全二叉树就成为一个堆。
堆的类定义和筛选法
template<class T>
class MinHeap {
//最小堆类定义
private:
T* heapArray;//存放堆数据的数组
int CurrentSize;//当前堆中的元素数组
int MaxSize;//最大元素数目
void swap(int pos_x, int pos_y);//交换位置x和y的元素
void BuildHeap();//建堆
public:
MinHeap(const int n);//构造函数,参数n为堆的最大元素数目
virtual ~MinHeap() { delete[]heapArray; };//析构函数
bool isEmpty();//判断堆是否为空
bool isLeaf(int pos)const;//判断是否是叶结点
int LeftChild(int pos)const;//返回左孩子的位置
int RightChild(int pos)const;//返回右孩子的位置
int Parent(int pos)const;//返回父结点位置
bool Remove(int pos, T& node);//删除给定下标的元素
bool Insert(const T& newNode);//向堆中插入新元素newNode
T& RemoveMin();//从堆顶删除最小值
void SiftUp(int position);//从position开始向上调整
void SiftDown(int left);//从left开始向下筛选
};
template<class T>
MinHeap<T>::MinHeap(const int n) {
if (n <= 0)return;
CurrentSize = 0;
MaxSize = n;//最大元素数目赋值为n
heapArray = new T[MaxSize];//创建堆空间
//此处进行堆元素的赋值工作
BuildHeap();
}
template<class T>
bool MinHeap<T>::isLeaf(int pos)const {
//判断是否为叶结点
return(pos >= CurrentSize / 2) && (pos < CurrentSize);
}
template<class T>
void MinHeap<T>::BuildHeap() {
//建堆
for (int i = CurrentSize / 2 - 1; i >= 0; i--)
SiftDown(i);
}
template<class T>
int MinHeap<T>::LeftChild(int pos)const {
//返回左孩子位置
return 2 * pos + 1;
}
template<class T>
int MinHeap<T>::RightChild(int pos)const {
//返回右孩子位置
return 2 * pos + 2;
}
template<class T>
int MinHeap<T>::Parent(int pos)const {
return (pos - 1) / 2;
}
template<class T>
bool MinHeap<T>::Insert(const T& newNode) {
//向堆中插入新元素newNode
if (CurrentSize == MaxSize)//如果堆已满则返回FALSE
return false;
heapArray[CurrentSize] = newNode;//新元素放到堆的末尾
SiftUp(CurrentSize);//向上调整
CurrentSize++;//堆的当前元素数加1
return true;
}
template<class T>
T& MinHeap<T>::RemoveMin() {
//从堆顶删除最小值
if (CurrentSize == 0) {
cout << "Can't Delete";
exit(1);
}
else {
swap(0, --CurrentSize);//交换堆顶和堆末尾的元素
if (CurrentSize > 1)//若当前元素数大于1则从堆顶开始向下筛选
SiftDown(0);
return heapArray[CurrentSize];
}
}
template<class T>
bool MinHeap<T>::Remove(int pos, T& node) {
//删除给定下标的元素
if ((pos < 0) || pos >= CurrentSize)
return false;
node = heapArray[pos];//记录删除的元素
heapArray[pos] = heapArray[--CurrentSize];//用最后的元素替代被删除元素
if (heapArray[Parent(pos)] > heapArray[pos])//当前元素小于父结点,需要上升调整
SiftUp(pos);
else SiftDown(pos);//当前元素大于父结点,向下筛选
return true;
}
template<class T>
void MinHeap<T>::SiftUp(int position) {
//从position开始向上调整
int temppos = position;
T temp = heapArray[temppos];
while ((temppos > 0) && (heapArray[Parent(temppos)] > temp)) {
heapArray[temppos] == heapArray[Parent(temppos)];
temppos = Parent(temppos);
}heapArray[temppos] = temp;
}
template<class T>
void MinHeap<T>::SiftDown(int left) {
//从left开始向下筛选
int i = left;//标识父结点
int j = LeftChild(i);//用于记录关键值较小的子结点
T temp = heapArray[i];//保存父结点
while (j < CurrentSize) {
//过筛
if ((j < CurrentSize - 1) && (heapArray[j] > heapArray[j + 1]))
//若有右子结点且小于左子结点
j++;//j指向右子结点
if (temp > heapArray[j]) {
//如果父结点大于子结点的值则交换位置
heapArray[i] = heapArray[j];
i = j;
j = LeftChild(j);
}
else break;//堆序性满足时跳出
}heapArray[i] = temp;
}
SiftDown()函数的时间复杂度是O(logn)。
对于n个结点的堆,其对应的完全二叉树的层数为logn。
最小堆只适合查找最小值,查找任意值的效率不高。
类似的也可以定义最大堆。
优先队列
首先要包含头文件#include, 他和queue不同的就在于我们可以自定义其中数据的优先级, 让优先级高的排在队列前面,优先出队。
优先队列具有队列的所有特性,包括队列的基本操作,只是在这基础上添加了内部的一个排序,它本质是一个堆实现的。
和队列基本操作相同:
top 访问队头元素
empty 队列是否为空
size 返回队列内元素个数
push 插入元素到队尾 (并排序)
emplace原地构造一个元素并插入队列
pop 弹出队头元素
swap 交换内容
定义:priority_queue<Type, Container, Functional>
Type 就是数据类型,Container 就是容器类型(Container必须是用数组实现的容器,比如vector,deque等等,但不能用 list。STL里面默认用的是vector),Functional 就是比较的方式。
当需要用自定义的数据类型时才需要传入这三个参数,使用基本数据类型时,只需要传入数据类型,默认是大顶堆。
一般是:
//升序队列,小顶堆
priority_queue <int,vector<int>,greater<int> > q;
//降序队列,大顶堆
priority_queue <int,vector<int>,less<int> >q;
//greater和less是std实现的两个仿函数(就是使一个类的使用看上去像一个函数。其实现就是类中实现一个operator(),这个类就有了类似函数的行为,就是一个仿函数类了)
Huffman树
外部路径长度:从扩充二叉树的根结点到每一个外部结点的路径长度之和。
在外部结点数相同的情况下,完全二叉树能够使得外部路径长度最小。
结点的带权路径长度是指从根结点到该结点的路径长度与结点权值的乘积。
带权外部路径长度(wighted external path)就是外部结点的带权路径长度之和。
图
基础知识
通常用n表示图中顶点的数目,用e表示边或弧的数目。
无向图中e的取值范围是0~n(n-1)/2;
有向图中e的取值范围是0~n(n-1);
完全图:任何两个顶点间都有边关联的图
度:
- 无向图中顶点v的度(degree)是与该顶点相关联的边的数目,记为D(v)。
- 如果G是一个有向图,则把顶点v为终点的弧的数目称为v的入度(in degree)记为ID(v);把以顶点v为始点的弧的数目称为v的出度(out degree),记为OD(v)。并把出度为0的顶点称为终端顶点(terminal)或叶子。
- 一般来说,如果图G中有n个顶点{v0,v1,…,vn},e条边,D(vi)为顶点vi的度数,有e = 1/2∑(i=0到n-1)D(vi)
路径长度length:定义为路径上的边(或弧)的数目。
回路或环cycle:第一个顶点和最后一个顶点相同的路径。
简单路径simple path:序列中顶点不重复出现的路径。
简单回路simple cycle:除了第一个顶点和最后一个顶点外,其余顶点不重复的回路。
无环图acyclic graph:不带回路的图。
有向无环图directed acyclic graph,DAG:不带回路的有向图。
连通图:如果对于无向图中任意两个顶点都是连通的。
连通分量:定义为无向图中的极大连通子图。
强连通图:对于有向图G=<V,E>,若G中任意两个顶点vi和vj,都有一条从vi到vj的有向路径,同时还有一条从vj到vi的有向路径,则称有向图G是强连通图。
连通图的生成树:含有该连通图全部顶点的一个极小连通子图。若连通图G的顶点个数为n,则G的生成树的边数为n-1.但是有n-1条边的图不一定是生成树。
如果无向图G的一个生成树G‘上添加一条边,则G‘一定有环,因为依附于这条边的两个顶点有另一条路径。相反,如果G‘的边数小于n-1,则G‘一定不连通。
自由树(free tree):是不带简单回路的无向图,它是连通的,并且具有n-1条边。
网络:是带权的连通图。
有向树:如果一个有向图只有一个顶点的入度为0,其余顶点的入度均为1,则称为有向树。
图的抽象数据类型
class Graph {
//图的抽象数据类型
public:
int VericesNum();//返回图的顶点个数
int EdgesNum();//返回图的边数
Edge FirstEdge(int oneVertex);//返回依附于顶点oneVertex的第一条边
Edge NextEdge(int preEdge);//返回与preEdge有相同顶点的下一条边
bool setEdge(int fromVertex, int toVertex, int weight);//添加边
bool delEdge(int fromVertex, int toVertex);//删除边
bool isEdge(Edge oneEdge);//判断是否是边
int FromVertex(Edge oneEdge);//返回始点
int toVertex(Edge oneEdge);//返回终点
int Weight(Edge oneEdge);//返回权
};
图的存储结构
相邻矩阵
图的基类
class Edge {
//边类
public:
int from, to ,weight;//from是边的始点,to是终点,weight是边的权
Edge() {
//缺省构造函数
from = -1; to = -1; weight = 0;
}
Edge(int f, int t, int w) {//给定参数的构造函数
from = f; to = t; weight = w;
}
};
class Graph {
public:
int numVertex;//图中顶点个数
int numEdge;//图中边的条数
int* Mark;//标记图的顶点是否被访问过
int* Indegree;//存放图中顶点的入度
Graph(int numVert) {
numVertex = numVert;
numEdge = 0;
Indegree = new int[numVertex];
Mark = new int[numVertex];
for (int i = 0; i < numVertex; i++) {
Mark[i] = UNVISITED;///标志位设为UNVISITED
Indegree[i] = 0;//入度设为0
}
}~Graph() {
delete[]Mark;//释放Mark数组
delete[]Indegree;//释放Indegree数组
}
int VerticesNum() {
//返回图中顶点个数
return numVertex;
}
bool IsEdge(Edge oneEdge) {
//oneEdge是否是边
if (oneEdge.weight > 0 && oneEdge.weight < INFINITY && oneEdge.to >= 0)
return true;
return false;
}
};
用相邻矩阵表示图
class Graphm :public Graph {
private:
int** matrix;//指向相邻矩阵的指针
public:
Graphm(int numVert) :Graph(numVert) {
int i, j;
matrix = (int**)new int* [numVertex];//申请matrix数组行向量数组
for (i = 0; i < numVertex; i++)//申请matrix数组行的存储空间
matrix[i] = new int[numVertex];
for (i = 0; i < numVertex; i++)
for (j = 0; j < numVertex; j++)
matrix[i][j] = 0;//矩阵所有元素初始化为0
}
~Graphm() {
for (int i = 0; i < numVertex; i++)
delete[]matrix[i];//释放每个matrix[i]行申请的空间
delete[]matrix;//释放matrix指针指向的行向量空间
}
Edge FirstEdge(int oneVertex) {
//返回依附于顶点oneVertex的第一条边
Edge myEdge;
myEdge.from = oneVertex;//将顶点作为边的始点
for (int i = 0; i < numVertex; i++) {
//找第一个matrix[oneVertex][i]不为0的i
if (matrix[oneVertex][i] != 0) {
myEdge.to = i;
myEdge.weight = matrix[oneVertex][i];
break;//找到第一条边就跳出循环
}
}return myEdge;
}
Edge NextEdge(Edge preEdge) {
//返回与preEdge有相同顶点的下一条边
Edge myEdge;
myEdge.from = preEdge.from;//边的始点与preEdge的的始点相同
if (preEdge.to < numVertex) {
//如果preEdge.to+1>=numVertex,将不存在下一条边
for (int i = preEdge.to + 1; i < numVertex; i++) {
//找下一个matrix[oneVertex][i]不为0的
if (matrix[preEdge.from][i] != 0)
{
myEdge.to = i;
myEdge.weight = matrix[preEdge.from][i];
break;//找到下一条边就跳出循环
}
}
}return myEdge;
}
void setEdge(int from, int to, int weight){
//为图设置一条边
if (matrix[from][to] <= 0) {
//如果原边不存在则边数和终点入度加1
numEdge++;
Indegree[to]++;
}
matrix[from][to] = weight;//设置边的权为weight
}
void delEdge(int from, int to) {
//删除图的一条边
if (matrix[from][to] > 0) {
//如果原边存在,则边数和终点入度减一
numEdge--;
Indegree[to]--;
}matrix[from][to] = 0;//边权重修改为0
}
};
邻接表
当图中边数较少时,相邻矩阵就会出现大量的0元素,存储这些0元素将耗费大量的存储空间。对于稀疏图,可采用邻接表存储法。
**邻接表(adjacency list)**表示法是一种链式存储结构,有一个顺序存储的顶点表和n个链接存储的边表组成。
顶点表目有两个域:顶点数据域和指向此顶点边表指针域。
边表把依附于同一个顶点vi的边(即相邻矩阵中同一行的非零元素)组织成一个单链表。边表中的每一个表目都代表一条边,由两个主要的域组成:与顶点vi邻接的另一顶点的序号、指向边表中下一个边表的目的指针。
顶点vi的边表的表目个数是该顶点的出度。,因此邻接表也称为出边表。
边表中表目顺序往往按照顶点编号从小到大排列。
下面代码给出了图的邻接表表示法的类定义和实现,没有考虑无向图对称边的情况。
struct listUnit {
int vertex;//邻接表中边结点的结构体定义
int weight;//边的权
};
template<class Elem>
class Link {
//链表元素类
public:
Elem element;//表目的数组
Link* next;//指向下一个链表元素的指针
Link(const Elem& elemval, Link* nextval = NULL) {
//构造函数
element = elemval;
next = nextval;
}
Link(Link* nextval = NULL) {
next = nextval;
}
};
template<class Elem>
class LList {
//链表类
public:
Link<Elem>* head;//定义一个头指针以方便操作
LList() {
head = new Link<Elem>();//构造函数
}
};
class Graphl :public Graph {
private:
LList<listUnit>* graList;//保存所有边表的数组
public:
Graphl(int numVert) :Graph(numVert) {
//构造函数
graList = new LList<listUnit>[numVertex];
}
Edge FistEdge(int oneVertex) {
//返回依附于顶点oneVertex的第一条边
Edge myEdge;
myEdge.from = oneVertex;//将顶点oneVertex作为边的始点
Link<listUnit>* temp = graList[oneVertex].head;//temp指向边表的第一个元素
if (temp->next != NULL) {
//如果顶点oneVertex边表非空
myEdge.to = temp->next->element.vertex;
myEdge.weight = temp->next->element.weight;
}return myEdge;
}
Edge NextEdge(Edge preEdge) {
//返回与preEdge有相同顶点的下一条边
Edge myEdge;
myEdge.from = preEdge.from;
Link<listUnit>* temp = graList[preEdge.from].head;//temp指向边表的第一个元素
while (temp->next != NULL && temp->next->element.vertex <= preEdge.to)
temp = temp->next;//确定preEdge的位置
if (temp->next!= NULL) {
//如果preEdge的下一条边存在
myEdge.to = temp->next->element.vertex;
myEdge.weight = temp->next->element.weight;
}
return myEdge;
}
void setEdge(int from, int to, int weight) {
//为图设置一条边
Link<listUnit>* temp = graList[from].head;//temp指向边表的第一个元素
while (temp->next!=NULL && temp->next->element.vertex < to)
temp = temp->next;//确定边(from,to)在边表中的位置
if (temp->next == NULL) {
//如果边(from,to)不存在且是最后一条边
temp->next = new Link<listUnit>;//在边表最后加入边(from,to)
temp->next->element.vertex = to;
temp->next->element.weight = weight;
numEdge++;//边数加1
Indegree[to]++;//终点的入度加1
return;
}
if (temp->next->element.vertex == to) {
//如果边(from,to)在边表中已存在
temp->next->element.weight = weight;//只需改变权值
return;
}
if (temp->next->element.vertex > to) {
//边(from,to)不存在且后面还有边
Link<listUnit>* other = temp->next;//临时存放指向后面链表元素的指针
temp->next = new Link<listUnit>;//在边表中插入边(from,to)
temp->next->element.vertex = to;
temp->next->element.weight = weight;
temp->next->next = other;//链接后面的链表元素
numEdge++;
Indegree[to]++;//终点入度加1
return;
}
}
void delEdge(int from, int to) {
//删掉图的一条边
Link<listUnit>* temp = graList[from].head;//temp指向边表的头结点,temp->next为第一个元素
while (temp->next != NULL && temp->next->element.vertex < to)
temp = temp->next;//确定边(from,to)在边表中的位置,为了删除方便
//temp指向的是被删除元素的前驱
if (temp->next == NULL)return;//temp的后继为空,则已经走到表尾,边不存在,返回
if (temp->next->element.vertex > to)return;
//边表的出边是按照编号排序的,边的终点标号如果超出参数to,则该边不存在,返回
if (temp->next->element.vertex == to) {
//边(from,to)在边表中存在
Link<listUnit>* other = temp->next->next;
delete temp->next;//从边表中删除
temp->next = other;//重新挂接其他链表元素
numEdge--;//边数减1
Indegree[to]--;//终点的入度减1
return;
}
}
图的周游
深度优先周游
图的深度优先搜索(depth-first search,DFS)类似于树的先根次序周游。
深度优先周游方法的特点是尽可能先对纵深方向进行搜索。
1若图G是连通图,则周游过程结束;
2否则,选择一个尚未访问的顶点作为新的源点进行深度优先搜索,直至图中所有顶点均被访问。
事实上,深度优先搜索的结果是沿着图的某一分支搜索,知道它的末端,然后回溯,沿着另一分支进行同样的搜索,以此类推。
图的深度优先周游(DFS)算法:
void DFS(Graph& G, int v) {
//深度优先搜索的递归实现
G.Mark[v] = VISITED;//将标记位设置为VISITED(被访问过)
Visit(G, v);//访问顶点v
for (Edge e = G.FirstEdge(v); G.IsEdge(e); e = G.NextEdge(e))
if (G.Mark[G.ToVertex(e)] == UNVISITED)
DFS(G, G.ToVertex(e));
}
广度优先周游
广度优先搜索(breadth-first search,BFS)。 其周游过程是:
从图中的某个顶点v出发,访问并标记了顶点v之后,横向搜索v的所有邻接点u1,u2…ut。在依次访问v的各个未被访问的邻接点之后,再从这些邻接点出发,依次访问与它邻接的所有未曾访问过的顶点。重复上述过程直至图中所有与源点v有路径相通的顶点都被问过为止。
若G是连通图,则周游完成;否则,在图G中选一个尚未访问的顶点作为新源点继续广度优先搜索。
广度优先搜索的过程类似于树的按层次次序周游。可以使用FIFO队列保存已访问过的顶点,从而使得先访问的顶点的邻接点在下一轮被优先访问到。在搜索过程中,每访问到一个顶点后将其入队,当队头元素出队时将其未被访问的邻接点入队,每个顶点只入队一次。
图的广度优先周游(BFS)算法:
void BFS(Graph& G, int v) {
using std::queue;//使用STL中的队列
queue<int>Q;
Visit(G, v);//访问顶点v
G.Mark[v] = VISITED;//将标记位设置为VISITED
Q.push(v);//顶点v入队列
while (!Q.empty()) {
//如果队列为空
int u = Q.front();//获得队列头部元素
Q.pop();//队列头部元素出队
//与该顶点邻接的所有未访问过的顶点入队
for(Edge e=G.FirstEdge(u);G.IsEdge(e);e=G.NextEdge(e))
if (G.Mark[G.ToVertex(e)] == UNVISITED) {
Visit(G, G.ToVertex(e));
G.Mark[G.ToVertex(e)] == VISITED;
Q.push(G.ToVertex(e));
}
}
}
拓扑排序
拓扑序列(topological order):存在顶点vi到vj的一条路径,那么在序列中顶点vi必在必在顶点vj之前,顶点集合V的线性序列称作拓扑序列。
拓扑排序(topological sorting):根据有向图建立拓扑序列的过程称为拓扑排序。
进行有向图的拓扑排序方法如下:
- 从有向图中选出一个没有前驱(入度为0)的顶点并输出。
- 删除图中该顶点和所有以它为起点的弧。
不断重复两个步骤。当图中的顶点全部输出时,就完成了有向无环图的拓扑排序;当图中还有顶点没有输出时,说明有向图中含有环。
可见,拓扑排序可以检查有向图是否存在环。
下面用邻接表作为有向图的存储结构来实现有向图的拓扑排序。每个顶点中加入一个存储该顶点的入度的域(indegree)。
为了减少查找度为0的顶点的次数,可把入度为0的顶点构造成一个队列,使得每次查找入度为0的顶点时只要从队列中取出第一个顶点即可,而不必检查整个顶点表。删除入度为0的顶点,如果此时某个顶点的入度减为0,就将其插入队列。
void TopsortbyQueue(Graph& G) {
for (int i = 0; i < G.VerticesNum(); i++)//初始化Mark数组
G.Mark[i] = UNIVISITED;
using std::queue; //使用STL中的队列
queue<int>Q;
for (int i = 0; i < G.VerticesNum(); i++)//入度为0的顶点入队
if (G.Indegree[i] == 0)
Q.push(i);
while (!Q.empty()) {//如果队列非空
int v = Q.front();//获得队列顶部元素
Q.pop(); //队列顶部元素出队
Visit(G, v);//访问顶点v
G.Mark[v] = VISITED;//将标记位设置为VISITED
for (Edge e = G.FirstEdge(v); G.IsEdge(e); e = G.NextEdge(e)) {
G.Indegree[G.ToVertex(e)]--;//与该顶点相邻的顶点入度减1
if (G.Indegree[G.ToVertex(e)] == 0)//如果顶点入度减为0则入队
Q.push(G.ToVertex(e));
}
}for(int i=0;i<G.VerticesNum();i++)//利用标记位可判断图中是否有环
if (G.Mark[i] == UNVISITED) {
cout << "此图有环!";
break;
}
}
最短路径
一般称路径上的第一个顶点为源点(source),最后一个顶点为汇点(sink),或终点(destination)。
单源最短路径
//Dijkstra算法
class Dist {
//Dist类,Dijkstra和Floyd算法用于保存最短路径信息
public:
int index;//顶点的索引值,仅Dijkstra算法用到
int length;//当前最短路径长度
int pre;//路径最后经过的顶点
};
void Dijkstra(Graph& G, int s, Dist*& D) {
//s是源点
D = new Dist[G.VerticesNum()];//数组D记录当前找到的最短特殊路径长度
for (int i = 0; i < G.VerticesNum(); i++) {
//初始化Mark数组、D数组
G.Mark[i] = UNVISITED;
D[i].index = i;
D[i].length = INFINITE;
D[i].pre = s;
}
D[s].length = 0;//源点到自身的路径长度设置为0
MinHeap<Dist>H(G.EdgesNum());//最小值堆用于找出最短路径
H.Insert(D[s]);
for (int i = 0; i < G.VerticesNum(); i++) {
bool FOUND = false;
Dist d;
while (!H.isEmpty()) {
d = H.RemoveMin();//获得到s路径长度最小的顶点
if (G.Mark[d.index] == UNVISITED) {
//如果未访问过则跳出循环
FOUND = true;
break;
}
}if (!FOUND)//若没有符合条件的最短路径则跳出本次循环
break;
int v = d.index;
G.Mark[v] = VISITED;//将该顶点的标记位设置为VISITED
//加入v以后需要刷新D中v的邻接点的最短路径长度
for(Edge e=G.FirstEdge(v);G.IsEdge(e);e=G.NextEdge(e))
if (D[G.ToVertex(e)].length > (D[v].length + G.Weight(e))) {
D[G.ToVertex(e)].length = D[v].length + G.Weight(e);
D[G.ToVertex(e)].pre = v;
H.Inset(D[G.ToVertex(e)]);
}
}
}
对于n个顶点e条边的图,图中的任何一条边都可能在最短路径中出现,因此最短路径算法对每条边至少都要检查一次。
上述算法采用最小堆来选择权值最小的边,因此每次改变最短特殊路径长度时需要对堆进行一次重排,此时的时间复杂度为O((n+e)loge),适合于稀疏图。
每对顶点之间的最短路径
设置一个nxn的矩阵path,path[i,j]是由顶点vi到顶点vj的最短路径上排在顶点vj前面的那个顶点,即当k在Floyd算法中使得adj(k)[i,j]达到最小值,就置path[i,j]=k。如果当前没有最短路径,就将path[i,j]置为-1.
void Floyd(Graph& G, Dist**& D) {
int i, j, v;
D = new Dist * [G.VerticesNum()];//为数组D申请空间
for (i = 0; i < G.VerticesNum(); i++)
D[i] = new Dist[G.VerticesNum()];
//初始化数组D
for(i=0;i<G.VerticesNum();i++)
for (j = 0; j < G.VerticesNum(); j++) {
if (i == j) {
D[i][j].length = 0;
D[i][j].pre = i;
}
else {
D[i][j].length = INFINITE;
D[i][j].pre = -1;
}
}
for(v=0;v<G.VerticesNum();v++)
for (Edge e = G.FirstEdge(v); G.IsEdge(e); e = NextEdge(e)) {
D[v][G.ToVertex(e)].length = G.Weight(e);
D[v][G.ToVertex(e)].pre = v;
}
//顶点i到顶点j的路径经过顶点v如果变短,则更新路径长度
for(v=0;v<G.VerticesNum();v++)
for(i=0;i<G.VerticesNum();i++)
for(j=0;j<G.VerticesNum();j++)
if (D[i][j].length > (D[i][v].length + D[v][j].length)) {
D[i][j].length = D[i][v].length + D[v][j].length;
D[i][j].pre = D[v][j].pre;
}
}
最小生成树
图G的生成树是一棵包含G的所有顶点的树,树上所有权值总和表示代价,在G的所有生成树中,代价最小的生成树称为图G的最小生成树(minimum-cost spanning tree ,MST).
构造最小生成树有多种算法。下面介绍的构造最小生成树Prim算法和Kruskal算法都是贪心算法。
Prim算法
//Prim算法
void Prim(Graph& G, int s, Edge*& MST) {
//s是开始顶点,数组MST用于保存最小生成树的边
int MSTtag = 0;//最小生成树的边记数
MST = new Edge[G.VerticesNum() - 1];//为数组MST申请空间
Dist* D;
D = new Dist[G.VerticesNum()];//为数组D申请空间
for (int i = 0; i < G.VerticesNum(); i++) {
//初始化Mark数组、D数组
G.Mark[i] = UNVISITED;
D[i].index = i;
D[i].lengh = INFINITE;
D[i].pre = s;
}
D[s].length = 0;
G.Mark[s] = VISITED;//开始顶点标记设置为VISITED
int v = s;
for (int i = 0; i < G.VerticesNum() - 1; i++) {
if (D[v] == INFINITY)return;//非连通,有不可达顶点
//因为v的加入,需要刷新与v相邻接的顶点的D值
for(Edge e=G.FirstEdge(v);G.IsEdge(e);e=G.NextEdge(e))
if (G.Mark[G.ToVertex(e)] != VISITED && (D[G.ToVertex(e)].lenght > e.weight)) {
D[G.ToVertex(e)].length = e.weight;
D[G.ToVertex(e)].pre = v;
}
v = minVertex(G, D);//在D数组中找最小值记为v
G.Mark[v] = VISITED;//标记访问过
Edge edge(D[v].pre, D[v].index, D[v].length);//保存边
AddEdgetoMST(edge, MST, MSTtag++);//将边edge加到MST中
}
int minVertex(Graph & G, Dist * &D) {
//在Dist数组中找最小值
int i, v;
for(i=0;i<G.VerticesNum();i++)
if (G.Mark[i] == UNVISITED) {
v = i;//让v为随意一个未访问的结点
break;
}
for (i = 0; i < G.VerticesNum(); i++)
if ((G.Mark[i] == UNVISITED) && (D[i] < D[v]))
v = i;//保存当前发现的具有最小距离的顶点
return v;
}
}
Kruskal算法
Kruskal算法使用的贪心准则是是从剩下的边中选择不会产生环路且具有最小权值的边加入到生成树的边集中。
在Kruskal算法中,把连通分量中的顶点作为集合元素,采用第六章的并查集的Find算法确定边的两个关联顶点所属的连通分支,采用Union算法合并两个连通分量。
在Kruskal算法中,需要按边权值递增的顺序依次查看边,可以把按边的权值组织优先队列,权值越小,优先级就越高。用最小堆来实现这个优先队列。
在Kruskal算法中,把连通分量中的顶点作为集合元素,采用并查集的Find算法确定边的两个关联顶点所属的连通分支,采用Union算法合并两个连通分量。
在Kruskal算法中,需要按边权值递增的顺序依次查看边,可以把按边的权值组织优先队列,权值越小,优先级就越高。用最小堆来实现这个优先队列。
void Kruskal(Graph& G, Edge*& MST) //数组MST用于保存最小生成树的边
{
ParTree<int>A(G.VerticesNum());//等价类
MinHeap<Edge>H(G.VerticesNum());//最小堆
MST = new Edge[G.VerticesNum() - 1];//为数组MST申请空间
int MSTtag = 0;//最小生成树的边计数
bool heapEmpty;
for (int i = 0; i < G.VerticesNum(); i++)//将图的所有边插入最小堆H中
for (Edge e = G.FirstEdge(i); G.IsEdge(e); e = G.NextEdge(e))
if (G.FromVertex(e) < G.ToVertex(e))//对于无向图,防止重复插入边
H.Insert(e);
int EquNum = G.VerticesNum();//开始n个顶点分别作为一个等价类
while (EquNum > 1) {
//当等价类的个数大于1时合并等价类
heapEmpty = H.isEmpty();
if (!heapEmpty)
Edge e = H.RemoveMin();//获得一条权最小的边
if (heapEmpty || e.weight == INFINITY) {
cout << "不存在最小生成树" << endl;
delete[]MST;//释放空间
MST = NULL;//MST赋为空
return;
}
int from = G.FromVertex(e);//记录该边的信息
int to = G.ToVertex(e);
if (A.Diferent(from, to)) {
//边e的两个顶点不在一个等价类
A.Union(from, to);//将边e的两个顶点所在的等价类合并为一个
AddEdgetoMST(e, MST, MSTtag++);//将边e加到MST
EquNum--;//等价类的个数减1
}
}
}
int Find(int* parent, int f)
{
while (parent[f] > 0)
f = parent[f];
return f;
}
#define MAXEDGE 100
#define MAXVEX 100
//Kruskal算法生成最小生成树
void MiniSpanTree_Kruskal(Graph G)
{
int i, n, m;
Edge edges[MAXEDGE];//定义边集数组
int parent[MAXVEX];//定义parent数组用来判断边与边是否形成环路
for (i = 0; i < G.numVertex; i++)
parent[i] = 0;
for (i = 0; i < G.numEdge; i++)
{
n = Find(parent, edges[i].from);
m = Find(parent, edges[i].to);
if (n != m)//如果n==m,则形成环路,不满足
{
parent[n] = m;//将此边的结尾顶点放入下标为起点的parent数组中,表示此顶点已经在生成树集合中
}
}
}
Kruskal算法的时间复杂度为O(eloge)。这个算法的时间复杂度主要取决于边数,因此Kruskal算法适合于构造稀疏图的最小生成树。
内排序
基本概念
- 记录(Record): 结点,进行排序的基本单位。
- 关键码(Key) 唯一确定记录的一个或多个域。
- 排序码(Sort Key) 作为排序运算依据的一个或多个域。
- 序列(Sequence) 线性表–由记录组成。
- 排序(Sorting) 将序列中的记录按照排序码特定的顺序排列起来,即排序码域的值具有不减(或不增)的顺序。
- 内排序(Internal Sorting) 整个排序过程中所有的记录都可以直接存放在内存中。
- 外排序(External Sorting) 内存无法容纳所有记录,排序过程中还需要访问外存。
- “稳定的”(stable)排序算法: 如果存在多个具有相同排序码的记录,经过排序后这些记录的相对次序仍然保持不变。
总的排序类
template<class Record>
class Sorter {
//总排序类
protected:
//交换数组中的两个元素
static void swap(Record Array[], int i, int j);
public:
//对数组Arra进行排序
void Sort(Record Array[], int n);
//输出数组内容
void PrintArray(Record array[], int n);
};
//swap函数
template<class Record>
void Sorter <Record>::swap(Record Array[], int i, int j) {
//交换数组中的两个元素
Record TempRecord = Array[i];
Array[i] = Array[j];
Array[j] = TempRecord;
}
template <class Record>
void Sorter<Record>::PrintArray(Record Array[], int n) {
//输出数组内容
for (int i = 0; i < n; i++)
cout << Array[i] << " ";
cout << endl;
}
三种O(n^2)的简单排序
插入排序
逐个处理待排序的记录,每个新纪录都要与前面那些已排好序的记录进行比较,然后插入到适当的位置。
直接插入排序(Insert Sort)
template <class Record>
void InsertSort(Record Array[], int n) {
//Array[]为待排序数组,n为数组长度
Record TempRecord;//临时变量
//依次插入第i个记录
for (int i = 1; i < n; i++) {
TempRecord = Array[i];//从i开始往前寻找记录i的正确位置
int j = i - 1;//将那些大于等于记录i的记录后移
while ((j >= 0) && (TempRecord < Array[j]))
{
Array[j + 1] = Array[j];
j = j - 1;
}//此时j后面就是记录i的正确位置,回填
Array[j + 1] = TempRecord;
}
}
算法分析
- 稳定
- 空间代价O(1)
- 时间代价
最佳情况:n-1次比较,2(n-1)次移动,O(n)
最差情况:O(n^2)
平均情况:O(n^2)
冒泡排序
template<class Record>
void BubbleSort(Record Array[], int n) {
//Array[]为待排序数组,n为数组长度
bool NoSwap;//是否发生交换的标志
for (int i = 0; i < n - 1; i++) {
NoSwap = true;//标志初始为真
for(int j=n-1;j>i;j--)
if (Array[j] < Array[j - 1]) {
//如果发生了交换,标志变为假
swap(Array, j, j - 1);
NoSwap = false;
}//如果没有发生过交换,表示已经排好序,结束算法
if (NoSwap)
return;
}
}
- 稳定
- 空间代价O(1)
- 时间代价
最佳情况:一轮循环,(n-1)次比较,O(n)
最差情况:O(n^2)
平均情况:O(n^2)
直接选择排序
template<class Record>
void StraightSelectSort(Record Array[], int n)
{
//Array[]为待排序数组,n为数组长度
//依次选出第i小的记录,即剩余记录中最小的那个
for (int i = 0; i < n - 1; i++) {
//首先假设记录i就是最小的
int Smallest = i;
//开始向后扫描所有剩余记录
for (int j = i + 1; j < n; j++)
//如果发现更小的记录,记录它的位置
if (Array[j] < Array[Smallest])
Smallest = j;
//将第i小的记录放在数组中第i个位置
swap(Array, i, Smallest);
}
}
Shell排序
直接插入排序的两个性质:
- 在最好情况(序列本身已是有序的)下时间代价为O(n)
- 对于短序列,直接插入排序比较有效。
Shell排序有效地利用了直接插入排序的这两个性质。
算法思想
- 先将序列转化为若干小序列,在这些小序列内进行插入排序;
- 逐渐扩大小序列的规模,而减少小序列个数,使得待排序序列逐渐处于更有序的状态。
- 最后对整个序列进行扫尾直接插入排序,从而完成排序。
增量每次除于2递减的Shell排序
template <class Record>
void ShellSort(Record Array[], int n) {
//Shell排序,Array[]为待排序数组,n为数组长度
int i, delta;
for (delta = n / 2; delta > 0; delta /= 2)//增量delta每次除以2递减
for (i = 0; i < delta; i++)//分别对delta个子序列进行插入排序
ModInsSort(&Array[i], n - i, dalta);//“&”传Array[i]的地址,待处理数组长度为n-i
//如果增量序列不能保证最后一个delta间距为1,可以安排下面这个扫尾性质的插入排序
//ModInsSort(Array,n,1);
}
template<class Record>
void ModInsSort(Record Array[], int n, int delta) {
//修改的插入排序算法,参数delta表示当前的增量
int i, j;
for(i=delta;i<n;i+=delta)//对子序列中第i个记录,寻找合适的插入位置
for (j = i; j >= delta; j -= delta) {//j以dealta为步长向前寻找逆置对进行调整
if (Array[j] < Array[j - delta])//Array[j]<Array[j-delta],则二者为逆置对
swap(Array, j, j - delta);//交换Array[j]和Array[j-delta]
else break;
}
}
- 不稳定
- 空间代价: O(1)
- 时间代价:O(n^2)
- 选择适当的增量序列,可以使得时间代价接近O(n)
基于分治法的排序
将原始数组分为若干个子部分然后分别进行排序
基本思想
分治策略的实例:
- 快速排序,归并排序。
- BST查找、插入、删除算法。
- 二分检索。
主要思想:
- 分——划分子问题
- 治——求解子问题(子问题不重叠)
- 合——综合解
快速排序
- 选择轴值(pivot)
- 将序列划分为两个子序列L和R,使得L中所有记录都小于或等于轴值,R中记录都大于轴值。
- 对子序列L和R递归进行快速排序。
轴值选择
- 尽可能使L,R长度相等
- 选择策略
选择最左边记录
随机选择
选择平均值
分割过程
- 整个快速排序的关键
- 分割后使得L中所有记录位于轴值左边,R中记录位于轴值右边,即轴值已位于正确位置。
一次分割过程
- 选择轴值并存储轴值
- 最后一个元素放到轴值位置
- 初始化下标i,j,分别指向头尾
- i递增直到遇到比轴值大的元素,将此元素覆盖到j的位置;j递减直到遇到比轴值小的元素,将此元素覆盖到i的位置。
- 重复上一步直到i==j,将轴值放到i的位置,完毕。
template<class Record>
void QuickSort(Record Array[], int left, int right) {
//Array[]为待排序数组,i,j分别为数组两端
//如果子序列中只有0或1个记录,就不需排序
if (right <= left)return;
//选择轴值
int pivot = SelectPivot(left, right);
//分割前先将轴值放到数组末端
swap(Array, pivot, right);
//分割后轴值已到达正确位置
pivot = Partition(Array, left, right);
//对轴值左边的子序列进行递归快速排序
QuickSort(Array, left, pivot - 1);
//对轴值右边的子序列进行递归快速排序
QuickSort(Array, pivot + 1, right);
}
int SelectPivot(int left, int right) {
//选择轴值,参数left、right分别表示序列的左右端下标
return (left + right) / 2;//选择中间记录作为轴值
}
template<class Record>
int Partition(Record Array[], int left, int right) {
//分割函数,分后轴值已达到正确位置
int l = left, r = right;//l为左指针,r为右指针
//将轴值存放在临时变量中
Record TempRecord = Array[r];
while (l != r) {
//开始分割,l、r不断向中间移动,直到相遇
//l指针向右移动,越过那些小于等于轴值的记录,直到找到一个大于轴值的记录
while (Array[l] <= TempRecord && r > l)//"<="也可以改写为"<",但增加记录移动
l++;
if (l < r) {
//若l、r尚未相遇,将逆置元素换到后边的空位
Array[r] = Array[l];
r--;//r指针向左移动一步
}
//r指针向左移动,越过那些大于等于轴值的记录,直到找到一个小于轴值的记录
while (Array[r] >= TempRecord && r > l)
r--;
if (l < r) {
//若l、r尚未相遇,将逆置元素换到左边的空位
Array[l] = Array[r];
l++;
}
}
Array[l] = TempRecord;//把轴值回填到分界位置l上
return l;//返回分界位置l
}
//优化的快速排序
#define THRESHOLD 28
template <class Record>
void ModQuickSort(Record Array[], int left, int right)
{
//长子串处理
if (right - left + 1 > THRESHOLD) {
int pivot = SelectPivot(left, right);//选择轴值
swap(Array, pivot, right);//将轴值放在数组末端
pivot = Partition(Array, left, right);//分割
ModQuickSort(Array, left, pivot - 1);//处理左
ModQuickSort(Array, pivot + 1, right);//处理右
}
}
template<class Record>
void Quicksort(Record* Array, int n) {
//调用优化的递归快排,不处理小子串
ModQuickSort(Array, 0, n - 1);
//最后这个序列进行扫尾插入排序
InsertSort(Array, n);
}
归并排序
- 简单地将原始序列划分为两个序列。
- 分别对每个子序列递归排序。
- 最后将排好序的子序列合并为一个有序序列,即归并过程。
template<class Record>
void MergeSort(Record Array[], Record TempArray[], int left, int right)
{
//Array为待排序数组,left,right分别为数组两端
//如果序列中只有0或1个记录,就不用排序
if (left < right) {
//从中间划分为两个子序列
int middle = (left + right) / 2;
//对左边一半进行递归
MergeSort(Array, TempArray, left, middle);
//对右边一半进行递归
MergeSort(Array, TempArray, middle + 1, right);
//进行归并
Merge(Array, TempArray, left, right, middle);
}
}
template<class Record>
void Merge(Record Array[], Record TempArrayp[], int left, int right, int middle) {
//归并过程,将数组暂存临时数组
for (int j = left; j <= right; j++)
TempArray[j] = Array[j];
int index1 = left;//左边子序列的起始位置
int index2 = middle + 1;//右边子序列起始位置
int i = left;//从左开始归并
while ((index1 <= middle) && (index2 <= right))
{
//取较小者插入合并数组中
if (TempArray[index1] <= TempArray[index2])
Array[i++] = TempArray[index1++];
else
Array[i++] = TempArray[index2++];
}
//处理剩余记录
while (index1 <= middle)
Array[i++] = TempArray[index1++];
while (index2 <= right)
Array[i++] = TempArray[index2++];
}
优化的归并排序
#define THRESHOLD 28
template <class Record>
void ModMergeSort(Record Array[], Record TempArray[], int left, int right)
{
//Array为待排序数组,left,right两端
int middle;
//如果序列长度大于阈值(28最佳),递归进行归并
if (right - left + 1 > THRESHOLD) {
middle = (left + right) / 2;//从中间划分
//对左边一半进行递归
ModMergeSort(Array, TempArray, left, middle);
}
//小长度子序列进行插入排序,“&”传Array[left]的地址
else InsertSort(&Array[left], right - left + 1);
}
//优化的Sedgwick两个有序子序列归并
//右序列逆置了,都从两端向中间扫描,归并到新数组
template<calss Record>
void ModMerge(Record Array[], Record TempArray[], int left, int right, int middle) {
//归并过程
int index1, index2;//子序列的起始位置
int i, j, k;
for (i = left; i <= middle; i++)//复制左边的子序列
TempArray[i] = Array[i];
//复制右边的子序列,但顺序颠倒过来
for (j = 1; j <= right - middle; j++)
TempArray[right - j + 1] = Array[j + middle];
//开始归并,取较小者插入合并数组中
for (index1 = left, index2 = right, k = left; k <= right; k++)
//为保证稳定性,相等时左边优先
if (TempArray[index1] <= TempArray[index2])
Array[k] = TempArray[index1++];
else Array[k] = TempArray[index2--];
}
算法分析
空间代价O(n)
总时间代价O(nlogn)
不依赖原始数组的输入情况,最大最小以及平均时间代价均为O(nlogn)
稳定
优化:
- 同优化的快速排序一样,对基本已排序序列直接插入排序。
- R。Sedgewick优化:归并时从两端开始处理向中间推进,简化了边界判断。
堆排序
- **直接选择排序:**直接从剩余记录中线性查找最大记录
- 堆排序:基于最大值堆来实现,效率更高。
//堆排序算法
template<class Record>
void sort(Record Array[], int n) {
int i;
MaxHeap<Record>max_heap = MaxHeap<Record>(Array, n, n);//建堆
//算法操作n-1次,最小元素不需要出堆
for (i = 0; i < n - 1; i++)
//依次找出剩余记录中的最大记录,即堆顶
max_heap.RemoveMax();
}
算法分析
- 建堆:O(n)
- 删除堆顶:O(logn)
- 一次建堆,n次删除堆顶
- 总时间代价为O(nlogn)
- 空间代价为O(1)
各种排序算法的理论和实验时间代价
- 一个长度为n序列平均有n(n-1)/4对逆置。
- 任何一种只对相邻记录进行比较的排序算法的平均时间代价都是O(n^2)。
算法 | 最大时间 | 平均时间 | 最小时间 | 辅助空间代价 | 稳定性 |
---|---|---|---|---|---|
直接插入排序 | O(n^2) | O(n^2) | O(n) | O(1) | 稳定 |
冒泡排序 | O(n^2) | O(n^2) | O(n) | O(1) | 稳定 |
选择排序 | O(n^2) | O(n^2) | O(n^2) | O(1) | 不稳定 |
Shell排序(3) | O(n^(3/2)) | O(n^(3/2)) | O(n^(3/2)) | O(1) | 不稳定 |
快速排序 | O(n^2) | O(nlogn) | O(nlogn) | O(logn) | 不稳定 |
归并排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(n) | 稳定 |
堆排序 | O(nlogn) | O(nlogn) | O(nlogn) | O(1) | 不稳定 |
桶式排序 | O(n+m) | O(n+m) | O(n+m) | O(n+m) | 稳定 |
基数排序 | O(d*(n+r)) | O(d*(n+r)) | O(d*(n+r)) | O(n+r) | 稳定 |
- n很小或基本有序时插入排序比较有效。
- 综合性能快速排序最佳。
排序问题的下限
判定树(Dicision Tree)
- 判定树中叶结点的最大深度就是排序算法在最差情况下需要的最大比较次数。
- 叶结点的最小深度就是最佳情况下的最小比较次数。
小结
排序方法的分类
- 插入排序:直接插入排序,shell排序;
- 选择排序:直接选择排序,堆排序;
- 交换排序:冒泡排序,快速排序;
检索
基本概念
- 检索(search): 在一组记录集合中找到关键码值等于给定值的某个记录,或者找到关键码值符合特定条件的某些记录的过程。
查找表: 由同一类型的数据元素(或记录)构成的集合。
查找表可分为两类:
- 静态查找表:仅作查询和检索操作的查找表。
- 动态查找表: 有时在查询之后,还需要将“查询”结果为“不在查找表中”的数据元素插入到查找表中;或者从查找表中删除其“查询”结果为“在查找表中”的数据元素。
关键码:是数据元素(或记录)中某个数据项的值,用以标识(识别)一个数据元素(或记录)。
- 若此关键码可以识别唯一的一个记录,则称之为主关键码。
- 若此关键字能识别若干记录,则称之为次关键码。
检索:根据给定的某个值,在查找表中确定一个其关键码等于给定值的数据元素或(记录)。
若查找表中存在这样一个记录,则称“检索成功”。检索结果:给出整个记录的信息,或指示该记录在查找表中的位置;
否则则称检索不成功,检索结果:给出“空记录”或“空指针”。
如何进行检索
-
检索的效率非常重要
尤其对于大数据量
需要对数据进行特殊的存储处理 -
预排序
排序算法本身比较费时
只是预处理(在检索之前已经完成) -
建立索引
检索时充分利用辅助索引信息
牺牲一定的空间
从而提高检索效率 -
散列技术
把数据组织到一个表中
根据关键码的值来确定表中每个记录的位置 -
缺点
不适合进行范围查询
一般也不允许出现重复关键码
当散列方法不适合于基于磁盘的应用程序时,我们可以选择B树方法。
平均检索长度(ASL)
-
关键码的比较
检索运算的主要操作 -
平均检索长度(Average Search Leangth)
检索过程中对关键码需要执行的平均比较次数。
是衡量检索算法优劣的时间标准 -
ASL是存储结构中对象总数n的函数,其定义为:
线性表的检索
顺序检索
以顺序表或线性链表表示静态查找表。
//顺序检索的存储结构类型定义
template<class Type>
class Item {
private:
Type key;//关键码域
string other;//其他域
public:
Item(Type value) :key(value) {}
Type getKey() { return key; }//获取关键码值
void setKey(Type k) { key = k; }//设置关键码
};
template<class Type>
vector<Item<Type>*>dataList;
监视哨 顺序检索算法
//位置0用来做监视哨,位置1到length用来存储实际元素
//检索成功时返回元素位置,检索失败时统一返回0;
//顺序检索算法那
template<class Type>
int SeqSearch(vector<Item<Type>*>& dataList, int length, Type k)
{
int i = length;
//将第0个元素设为待检索值
//保证while循环一定终止
dataList[0]->setKey(k);
//从后往前逐个比较
while (dataList[i]->getKey() != k)i--;
return i;//返回元素位置
}
顺序检索性能分析
检索失败
假设检索失败时都需要比较n+1次(设置了一个监视哨)
平均检索长度
假设检索成功的概率为p,检索失败的概率为q=(1-p),则平均检索长度为
优点
- 存储:可以顺序,链接
- 排序要求:无
- 插入元素可以直接加在表尾。
缺点
检索时间太长O(n)
二分检索(折半查找)
上述顺序检索算法简单,但平均检索长度较大,特别不适用于表长较大的查找表。
若以有序表表示静态查找表,则检索过程可以基于“折半”进行。
二分检索算法
template <class Type>
int BinSearch(vector<Item<Type>*>& dataList, int length, Type k)
{
//low,high分别记录数组首尾位置
int low = 1, high = length, mid;
while (low <= high) {
mid = (low + high) / 2;
if (k < dataList[mid]->getKey())
high = mid - 1;//右缩检索区间
else if (k > dataList[mid]->getKey())
low = mid + 1;//左缩检索区间
else return mid;//检索成功,返回元素位置
}return 0;//检索失败,返回0
}
优点
平均检索长度与最大检索长度相近,检索速度快。
缺点
要排序、顺序存储,不易更新(插/删)
分块检索
- 顺序检索与二分检索的折衷
既有较快的检索
又有较灵活的更改
分块检索思想
-
按块有序
设线性表中共有n个数据元素,将表分成b块
不需要均匀
每一块可能不满
每一块中的关键码不一定有序,但前一块中的最大关键码必须小于后一块中的最小关键码 -
索引表
各块中的最大关键码
各块起始位置
可能还需要块中元素个数(每一块可能不满) -
索引表是一个递增有序表
因为表是分块有序的。
顺序表 | 有序表 | |
---|---|---|
表的特性 | 无序 | 有序 |
存储结构 | 顺序或链式 | 顺序 |
插删操作 | 易于进行 | 需移动元素 |
ASL的值 | 大 | 小 |
分块检索的平均检索长度ASL(n)=
检索“索引”的平均检索长度ASL(b)+
检索“顺序表”的平均检索长度ASL(w)
- 索引表是按块内最大关键码有序的,且长度也不大,可以二分检索,也可以顺序检索
- 各子表内各个记录不是按记录关键码有序,只能顺序检索。
- 假设在索引表中用顺序检索,在块内也用顺序检索。
若采用二分法检索确定记录所在的子表,则检索成功时的平均检索长度为:
分块检索的优缺点
优点
- 插入、删除相对较易
- 没有大量记录移动
缺点
- 增加一个辅助数组的存储空间
- 初始线性表分块排序
- 当大量插入/删除时,或结点分部不均匀时,速度下降
检索 | 插入 | 删除 | |
---|---|---|---|
无序顺序表 | O(n) | O(1) | O(n) |
无序顺序链表 | O(n) | O(1) | O(1) |
有序顺序表 | O(logn) | O(n) | O(n) |
有序线性链表 | O(n) | O(1) | O(1) |
静态查找树表 | O(logn) | O(nlogn) | O(nlogn) |
结论
- 从查找性能看,最好情况能达到O(logn),此时要求表有序。
- 从插入和删除性能看,最好情况能达O(1),此时要求存储结构是链表。
动态查找表——二叉搜索树
散列方法
要求:记录在表中位置和其关键码之间存在一种确定的关系。
对动态查找表而言
- 表长不确定;
- 在设计查找表时,只知道关键码所属范围,而不知道确切的关键码。
因此在一般情况下,需在关键码与记录在表中的存储位置之间建立一个函数关系,以f(key)作为关键字为key的记录在表中的位置,通常称这个函数f(key)为散列(哈希)函数。
- 散列(Hash)函数是一个映像,即:将关键码的集合映射到某个地址集合上, 它的设置很灵活,只要这个地址集合的大小不超出允许范围即可。
- 由于散列函数是一个压缩映像,因此,在一般情况下,很容易产生“冲突”现象,即,key1≠key2,而f(key1)=f(key2)。
- 很难招到一个不产生冲突的散列函数。一般情况下,只能选择恰当的哈希函数,使冲突尽可能少地产生。
几个重要概念
负载因子 a=n/m
- 散列表的空间大小为m;
- 填入表中的结点数为n;
冲突
- 某个散列函数对于不相等的关键码计算出了相同的散列地址;
- 在实际应用中,不产生冲突的散列函数极少存在。
同义词
- 发成冲突的两个关键码。
散列表的定义:
根据设定的散列函数H(key)和所选中的处理冲突的方法,
将一组关键码映象到一个有限的、地址连续的地址集(区间)上,
并以关键码在地址集中的“象”作为相应记录在表中的存储位置,
如此构造所得到的查找表称之为 散列表。
散列函数的选取原则
- 运算尽可能简单;
- 函数的值域必须在表长的范围内;
- 尽可能使得关键码不同时,其散列函数值亦不相同。
构造散列函数的方法
对数字的关键码可有下列构造方法:
- 除余法
- 乘余取整法
- 平方取中法
- 数字分析法
- 基数转换法
- 折叠法
- 随机数法
若是非数字关键字,则需先对其进行数字化处理。
除余法
设定散列函数为
hash(key) = key mod p
其中,p≤m(表长)并且p应为不大于m的素数或是不含20以下的质因子
除余法的潜在缺点
- 连续的关键码映射成连续的散列值
虽然能保证连续的关键码不发生冲突,但也意味着要占据连续的数组单元。可能导致散列性能的降低。
乘余取整法
平方取中法
以关键码的平方值的中间几位作为存储地址。
此方法适合于:
关键码中每一位都有某些数字重复出现频度很高的现象。
数字分析法
此方法仅适合于:
能预先估计出全体关键码的每一位上各种数字出现的频度。
基数转换法
- 把关键码看成是另一进制上的数后,再把它转换成原来进制上的数。
- 取其中若干位作为散列地址。
- 一般取大于原来基数的数作为转换的基数,并且两个基数要互素。
折叠法
将关键码分割成若干部分,然后取它们的叠加和为散列地址。
有两种叠加处理的方法:移位叠加和分界叠加。
此方法适合于:
关键码的数字位数特别多。
随机数法
设定散列函数为hash(key) = Random(key),其中,Random为伪随机函数。
通常,此方法用于对长度不等的关键码构造散列函数。
处理冲突的方法
处理冲突:为产生冲突的地址寻找下一个散列地址。
- 开散列法(拉链法):把发生冲突的关键码存储在散列表主表之外。
- 闭散列法(开地址法):把发生冲突的关键码存储在表中另一个槽内。
开散列法
将所有散列地址相同的记录(同义词)都l链接在同一链表中。
- 动态申请同义词的空间,适合于 内存操作。
- 如果整个散列表存储在内存中,开散列方法比较容易实现。
- 如果散列表存储在磁盘中,用开散列不太适合:一个同义词表中的元素可能存储在不同的磁盘页中,这就会导致在检索一个特定关键码值时引起多次磁盘访问,从而增加了检索时间。
闭散列法(开地址法)
为产生冲突的地址H(key)求得一个地址序列。
闭散列的算法
插入算法
template<class Key, class T>
class hashdict {
private:
T* HT;//散列表
int M;//散列表大小
int current;//现有元素数目
T EMPTY;//空槽
int p(Key, K, int i);//探查函数
int h(int x)const;//散列函数
int h(char* x)const;//字符串散列函数
public:
hashdict(int sz, T e) {
//构造函数
M = sz; EMPTY = E;
current = 0; HT = new T[sz];
for (int i = 0; i < M; i++)HT[i] = EMPTY;
}
~hashdict() { delete[]HT; }
bool hashSearch(const Key&, T&)const;
bool hashInsert(const T&);
T hashDelete(const Key& K);
int size() { return current; }//元素数目
};
//散列表插入算法
//将数据元素e插入到散列表HT中
template<class Key, class T>
bool hashdict<Key, T>::hashInsert(const T& e) {
int home = h(getkey(e));//home存储基位置
int i = 0;
int pos = home;//探查序列的初始位置
while (HT[pos] != EMPTY) {
if (getKey(HT[pos]) == getKey(e))return false;
i++;
pos = (home + p(getKey(e), i)) % M;//探查
}HT[pos] = e;//插入元素e
return true;
}
检索算法
template<class Key, class T>
bool hashdict<Key, T>::hashSearch(const Key& K, T& e)const
{
int i = 0, pos = home = h(K);//初始位置
while (HT[pos] != EMPTY)
{
if (getKey(HT[pos]) == K)
{
//找到
e = HT[pos];
return true;
}
i++;
pos = (home + p(K, i)) % M;
}return false;
}
删除算法
- 删除一个记录一定不能影响后面的检索。
- 释放的存储位置应该能够为将来插入使用。
- 只有开散列方法(分离的同义词子表)可以真正删除。
- 闭散列方法都只能作标记(墓碑)不能真正删除
墓碑标记增加了平均检索长度。
墓碑
- 设置一个特殊的标记位,来记录散列表中的单元状态。
-单元被占用;
-空单元;
-已删除 - 是否可以把空单元、已删除这两种状态,用特殊的值标记,以区别于“单元被占用”状态?
-不可以! - 被删除标记值称为墓碑(tombstone)
-标志一个记录曾经占用这个槽;
-但是现在已经不再占用了。
带墓碑的插入操作
- 在插入时,如果遇到标志位墓碑的槽,可以把新纪录存储在该槽中吗?
-避免插入两个相同的关键码;
-检索过程仍然需要沿着探查序列下去,直到找到一个真正的空位置。
带墓碑的删除算法
//带墓碑的删除算法
template<class Key,class T>
T hashdict<Key, T>::hashDelete(const Key& K) {
int i = 0, pos = home = h(K);//初始位置
while (HT[pos] != EMPTY) {
if (getKey(HT[pos]) == K)
{
temp = HT[pos];
HT[pos] = TOMB;//设置墓碑
return temp;//返回目标
}i++;
pos = (home + p(K, i)) % M;
}return EMPTY;
}
带墓碑的插入操作改进
template<class Key,class T>
bool hashdict<Key, T>::hashInsert(const T& e) {
int insplace, i = 0, pos = home = h(getKey(e));
bool tomb_pos = false;
while (HT[pos] != EMPTY) {
if (getKey(e) == getKey(HT[pos]))
return false;
if (HT[pos] == TOMB && !tomb_pos) {
insplace = pos;
tomb_pos = true;
}pos = (home + p(getKey(e), ++i)) % M;
}if (!tomb_pos)
insplace = pos;//没有墓碑
HT[inslace] = e;
return true;
}
散列方法的效率分析
决定散列表检索的ASL的因素:
- 选用的散列函数;
- 选用的处理冲突的方法;
- 散列表饱和的程度,装载因子;
α=n/m值的大小(n——记录数,m——表的长度)
-
散列表的平均查找长度是α的函数,而不是n的函数。
-
散列方法的代价一般接近于访问一个记录的时间,效率非常高,比需要logn次记录访问的二分检索好得多。
-
散列表的插入和删除操作如果很频繁,将降低散列表的检索效率。
-
实际应用中,对于插入和删除操作比较频繁的散列表,可以定期对表进行重新散列。
-把所有记录重新插入到一个新的表中
清除墓碑;
把最频繁访问的记录放到其基地址。