数据结构程序
1. 单链表
(1)实现一个单链表
【注】这里不是要求创建一个单链表类,否则会涉及到许多的类成员函数的定义,而这只是一个小编程题而已,不要小题大做。这里仅仅是要求建立一个(特定的)单链表而已。
#include <iostream>
using namespace std;
struct Node
{
int data;
Node *next;
};
int main()
{
int a[10] = {1,2,3,4,5,6,7,8,9,10};//笔试中的程序不必用scanf或cin进行输入,直接定义一个数组说明问题即可!
Node *head = new Node;
head->data = a[0];
Node *currPtr = head;
for(int i=1; i!=10; ++i)
{
Node * newNode = new Node;
newNode->data = a[i];
currPtr->next = newNode;
currPtr = currPtr->next;
}
currPtr->next = NULL; //【注】千万不要落掉这一条语句!
for(currPtr=head; currPtr!=NULL; currPtr=currPtr->next) //既然是链表,就应该用指针或迭代器来对齐遍历
cout << currPtr->data << " ";
cout << endl;
return 0;
}
(2)单链表的删除
① 方法一(更直接):
void deleteNode(Node *&head, const int item) //注意这里是对head的引用,从而可以返回新的链表首地址
{
Node *currPtr, *prevPtr;
for(currPtr=head; currPtr!=NULL && currPtr->data!=item; currPtr=currPtr->next)
prevPtr = currPtr;
if(currPtr == head) //第一种特殊情况
{
head = head->next;
delete currPtr;
}
else if(NULL == currPtr) //第二种特殊情况
cout << "can not find " << item << endl;
else //一般情况
{
prevPtr->next = currPtr->next;
delete currPtr;
}
}
② 方法二:
void deleteNode(Node *&head, const int item) //注意这里是对head的引用,从而可以返回新的链表首地址
{
Node *currPtr, *prevPtr;
for(currPtr=head; currPtr->next!=NULL && currPtr->data!=item; currPtr=currPtr->next)
prevPtr = currPtr;
if(currPtr->data == item)
{
if(currPtr == head)
{
head = head->next;
delete currPtr;
}
else
{
prevPtr->next = currPtr->next;
delete currPtr;
}
}
else
cout << "can not find " << item << endl;
}
(3)单链表的插入
① 方法一(更直接):
void insertNode(Node *&head, const int item)//注意这里是对head的引用,从而可以返回新的链表首地址
{
Node *currPtr, *prevPtr=NULL; //prevPtr是currPtr前一个结点的指针
Node *newNode = new Node;
newNode->data = item;
for(currPtr=head; currPtr!=NULL && newNode->data>currPtr->data; currPtr=currPtr->next)
prevPtr = currPtr;
//因为原链表是已排好序的,如果将“currPtr->data<newNode->data”放在循环中,则当该条件不满足时,
//循环仍在执行,但后面的都是冗余的。
//【注】在for循环的终止条件中,currPtr!=NULL必须在currPtr->data<newNode->data前面,否则,当currPtr==NULL时,不会 //再有currPtr->data,于是运行出错。这是由于“&&”前后的语句执行的顺行问题造成的,如果前面的条件不符合,后面的条 //件语句则不再执行。事实上,如果将currPtr->data<newNode->data放在循环内作为if条件的话就不会有这种情况了,不过这 //样会出现冗余的循环。
if(currPtr == head) //第一种特殊情况:新结点的值小于原链表头结点的值,新结点应插入表首(等价于NULL == prevPtr)
{
newNode->next = head;
head = newNode;
}
else if(NULL == currPtr) //第二种特殊情况:节点的值大于原链表最后一个结点的值,新结点应插入表尾
{
newNode->next = NULL;
prevPtr->next = newNode;
//【注】这里如果改为currPtr = newNode则为错误的,因为prevPtr->next仍然为NULL,而currPtr = newNode仅仅是修改
//了currPtr这个指针的值而已,而prevPtr->next并未改变(因为二者并未通过指针或引用相联系)。
}
else//新结点应插入链表中间
{
newNode->next = currPtr;
prevPtr->next = newNode;
}
}
② 方法二:
void insertNode(Node *&head, const int item)
{
Node *currPtr, *prevPtr;
Node *newNode = new Node;
newNode->data = item;
//第一个循环终止条件换成了“currPtr->next!=NULL”,从而避免了第一种方法可能出现的问题。
for(currPtr=head; currPtr->next!=NULL && newNode->data>currPtr->data; currPtr=currPtr->next)
prevPtr = currPtr;
//分两种情况,第一种是为违反了后一个循环终止条件而设立的。这时,新结点在当前节点前插入,
//而这又分为在表首和表中插入两种情况。
if(newNode->data <= currPtr->data)
{
if(currPtr == head)
{
newNode->next = head;
head = newNode;
}
else
{
newNode->next = currPtr;
prevPtr->next = newNode;
}
}
else//这一种是为违反了前一个循环终止条件而设立的。这时currPtr为链表的最后一个结点,而新结点要插入到其后面。
{
newNode->next = NULL;
currPtr->next = newNode;
}
}
(4)单链表的打印
void printList(const Node *head)
{
const Node *currPtr; //由于currPtr不是const指针,故不用在声明时初始化。
//这里指针类型必须为const Node *型,否则由于const Node *不能转化为Node *而导致语句“currPtr = head”编译出错。
for(currPtr = head; currPtr!=NULL; currPtr=currPtr->next)
cout << currPtr->data << " ";
cout << endl;
}
(5)单链表的测长
int sizeList(const Node *head)
{
int n = 0;
const Node *currPtr; //由于currPtr不是const指针,故不用在声明时初始化。
for(currPtr=head; currPtr!=NULL; currPtr=currPtr->next)
++n;
return n;
}
(6)单链表的排序
① 冒泡排序法:
void bubbleSort(Node *&head)
{
Node *p1, *p2; //用于排序
Node *lastExchangePtr; //冒泡排序法的主线
//遍历链表,使得p1指向链表的最后一个结点,从而为下面冒泡法的循环做准备
for(p1=head; p1->next!=NULL; p1=p1->next);
for(; p1!=head; p1=lastExchangePtr)
{
lastExchangePtr = head; //防止内循环没有执行以致lastExchangePtr没有改变从而导致死循环
for(p2=head; p2!=p1; p2=p2->next)
{
if(p2->data > p2->next->data)
{
swap(p2->data, p2->next->data);
lastExchangePtr = p2;
}
}
}
}
② 选择排序法:
void selectSort(Node *&head)
{
Node *p1, *p2, *smallPtr;
for(p1=head; p1->next!=NULL; p1=p1->next)
{
smallPtr = p1;
for(p2=p1->next; p2!=NULL; p2=p2->next)
if(p2->data < smallPtr->data)
smallPtr = p2;
swap(p1->data, smallPtr->data);
}
}
【注】单链表排序不能用插入排序法进行排序,因为插入排序法涉及到了逆序遍历。外层循环为:for(int i=1; i<n; ++i),而内层循环为:for(int j=i-1; j>0 && a[j]>temp; --j),其中“--j”在单链表中没法表示。
(7)单链表的逆置
① 方法一(更好):
void reverseList(Node *&head)
{
if(NULL==head || NULL==head->next) //特殊情况
return;
Node *prevPtr, *currPtr, *postPtr;
for(prevPtr=NULL,currPtr=head,postPtr=currPtr->next; currPtr->next!=NULL;
prevPtr=currPtr,currPtr=postPtr,postPtr=postPtr->next)//从第一个结点开始进行遍历,最后一个结点未连接
currPtr->next = prevPtr;
currPtr->next = prevPtr; //不要丢掉,因为最后一个结点没有和之前的结点像连接
head = currPtr;
}
② 方法二:
void reverseList(Node *&head)
{
if(NULL==head || NULL==head->next)
return;
Node *prevPtr, *currPtr, *postPtr;
prevPtr = head;
currPtr = head->next; //从第二个结点开始进行遍历,第一个结点未连接
while(currPtr != NULL)//以currPtr == NULL为判别终止条件
{
postPtr = currPtr->next; //仅当currPtr非NULL时才会有postPtr
currPtr->next = prevPtr; //每次逆置一次
prevPtr = currPtr; //指针后移,以便进行下一次循环
currPtr = postPtr;
}
head->next = NULL;
head = prevPtr;
}
(8)求单链表的中点
【要求】链表的结点数未知,怎样遍历一次就得出单链表的中间结点?
Node * searchMiddleNode(Node *head)
{
//空链表需要单独考虑,因为下面的一般情况要执行判别语句“p1->next != NULL”,
//而如果为空链表时,NULL->next会导致运行错误
if(NULL == head) //特殊情况
return NULL;
Node *p1, *p2; //用于循环
p1 = p2 = head;
//当head是只有一个结点的链表时,则返回head,即单结点链表的地址,于是不用单独进行特殊情况的考虑
while(p1->next != NULL) //如果要连续后移两个结点时,则必须分别进行后移,以防止出现NULL->next的情况
{
p1 = p1->next;
if(p1->next != NULL)
{
p1 = p1->next;
p2 = p2->next;
}
}
return p2;
}
(9)合并两个已排序的链表
【要求】合并两个从小到大已排好序的链表,并形成一个新的具有同样顺序的链表
//【合并思想】首先的确定下新的表头结点;其次,currPtr指向最近加入到新链表中的结点,而它所指向的
//是min(head1->data, head2->data)所对应的结点(假如是head1),进而currPtr指向该结点(最近加入到
//链表中的结点);而head1指向该结点的下一个结点,以便进行下一次比较。知道head1或head2为NULL时,
//而未空的那一个子链表直接连到currPtr后即可。
Node * uniteList(Node *head1, Node *head2)
{
Node *head = NULL;//新链表的表头
Node *currPtr=NULL, *smallPtr=NULL;
while(head1!=NULL && head2!=NULL)
{
if(head1->data < head2->data)
{
smallPtr = head1;
head1 = head1->next; //不要丢掉
}
else
{
smallPtr = head2;
head2 = head2->next; //不要丢掉
}
if(NULL == head)
{
head = smallPtr;
currPtr = head;//currPtr指向最近加入到新链表中的结点,即smallPtr
}
else
{
currPtr->next = smallPtr;
currPtr = currPtr->next;//currPtr指向最近加入到新链表中的结点,即smallPtr
}
}
//根据上面的循环终止条件,不是head1等于NULL就是head2等于NULL
if(NULL == head1)
currPtr->next = head2;
else
currPtr->next = head1;
return head;
}
2. 双链表
(1)实现一个双链表
双链表的实现在一定程度上与单链表相似,可参见前面的单链表的实现。
#include <iostream>
using namespace std;
struct DNode
{
int data;
DNode *left;
DNode *right;
};
int main()
{
DNode *head = new DNode;
int a[10] = {1,2,3,4,5,6,7,8,9,10};
head->data = a[0];
head->left = NULL;
DNode *currPtr = head;
for(int i=1; i!=10; ++i)
{
DNode *newNode = new DNode;
newNode->data = a[i];
newNode->left = currPtr;
currPtr->right = newNode;
currPtr = newNode;
}
currPtr->right = NULL;
for(currPtr=head; currPtr!=NULL; currPtr=currPtr->right)
cout << currPtr->data << " ";
cout << endl;
return 0;
}
(2)双链表的删除
① 方法一(更好、更简单):
void deleteDNode(DNode *&head, const int item)
{
DNode *currPtr, *prevPtr;
for(currPtr=head; currPtr!=NULL && currPtr->data!=item; currPtr=currPtr->right)
prevPtr = currPtr; // prevPtr并不是必须的,但是明显可以简化程序
if(currPtr == head)//第一种特殊情况
{
head = head->right;
head->left = NULL;
delete currPtr;
}
else if(NULL == currPtr)//第二种特殊情况
cout << "can not find " << item << endl;
else if(NULL == currPtr->right)//第三种特殊情况:双链表与单链表所不同之处:双链表的最后一个结点也是特殊情况
{
prevPtr->right = NULL;
delete currPtr;
}
else
{
prevPtr->right = currPtr->right;
currPtr->right->left = prevPtr;
delete currPtr;
}//与单链表的删除不同的是,双链表由于可以双向遍历,只需一个指针就可以完成上面的任务了
}
② 方法二:
void deleteDNode(DNode *&head, const int item)
{
DNode *currPtr;
for(currPtr=head; currPtr!=NULL && currPtr->data!=item; currPtr=currPtr->right);
//充分利用的双链表的性质,但是不如方法一简单
if(currPtr == head)//第一种特殊情况
{
head = head->right;
head->left = NULL;
delete currPtr;
}
else if(NULL == currPtr)//第二种特殊情况
cout << "can not find " << item << endl;
else if(NULL == currPtr->right)// 第三种特殊情况:双链表与单链表所不同之处:双链表的最后一个结点也是特殊情况
{
currPtr->left->right = NULL;
delete currPtr;
}
else
{
currPtr->left->right = currPtr->right;
currPtr->right->left = currPtr->left;
delete currPtr;
}//与单链表的删除不同的是,双链表由于可以双向遍历,只需一个指针就可以完成上面的任务了
}
③ 方法三(不直接):
void deleteDNode(DNode *&head, const int item)
{
DNode *currPtr;
for(currPtr=head; currPtr->right!=NULL && currPtr->data!=item; currPtr=currPtr->right);
if(item == currPtr->data)
{
if(currPtr == head)
{
head = head->right;
head->left = NULL;
}
else if(NULL == currPtr->right)
{
currPtr->left->right = NULL;
}
else
{
currPtr->left->right = currPtr->right;
currPtr->right->left = currPtr->left;
}
//与单链表的删除不同的是,双链表由于可以双向遍历,只需一个指针就可以完成上面的任务了
delete currPtr;
}
else
cout << "can not find " << item << endl;
}
(3)双链表的插入
① 方法一(更好、更简单):
void insertDNode(DNode *&head, const int item)
{
DNode *currPtr, *prevPtr;
DNode *newNode = new DNode;
newNode->data = item;
for(currPtr=head; currPtr!=NULL && newNode->data>currPtr->data; currPtr=currPtr->right)
prevPtr = currPtr;
if(currPtr == head)
{
newNode->right = currPtr;
newNode->left = NULL;
currPtr->left = newNode;
head = newNode;
}
else if(NULL == currPtr)
{
newNode->left = prevPtr;
newNode->right = NULL;
prevPtr->right = newNode;
}
else
{
newNode->right = currPtr;
newNode->left = prevPtr;
prevPtr->right = newNode;
currPtr->left = newNode;
}
}
② 方法二:
void insertDNode(DNode *&head, const int item)
{
DNode *currPtr;
DNode *newNode = new DNode;
newNode->data = item;
for(currPtr=head; currPtr->right!=NULL && newNode->data>currPtr->data; currPtr=currPtr->right);
//分两种情况,第一种是为违反了后一个循环终止条件而设立的。这时,新结点在当前节点前插入,
//而这又分为在表首和表中插入两种情况。
if(newNode->data <= currPtr->data)
{
if(currPtr == head)
{
newNode->right = currPtr;
newNode->left = NULL;
currPtr->left = newNode;
head = newNode;
}
else
{
newNode->right = currPtr;
newNode->left = currPtr->left;
currPtr->left->right = newNode;
currPtr->left = newNode;
}
}
//这一种是为违反了前一个循环终止条件而设立的。这时currPtr为链表的最后一个结点,
//而且由于newNode->data>currPtr->data,故新结点要插入到其后面。
else
{
newNode->right = NULL;
newNode->left = currPtr;
currPtr->right = newNode;
}
}
3. 循环链表
(1)实现一个循环链表
#include <iostream>
using namespace std;
struct CNode
{
int data;
CNode *next;
};
int main()
{
CNode *head, *currPtr, *newNode;
head = new CNode;
head->data = 0;
head->next = head;//循环链表的特色
currPtr = head;
for(int i=1; i<10; ++i)
{
newNode = new CNode;
newNode->data = i;
newNode->next = head; //循环链表的特色
currPtr->next = newNode;
currPtr = newNode;
}
int i;
for(currPtr=head,i=0; i<10; i++,currPtr=currPtr->next)
{
cout << currPtr->data << " ";
}
cout << endl;
}
(2)Josephus问题(约瑟夫问题)
已知n个人(以编号1,2,……,n分别表示)围坐在一张圆桌周围。从编号为k的人开始报数,数到m的那个人出列;他的下一个人又从1开始报数,数到m的那个人又出列;依此规律重复下去,直到圆桌周围的人全部出列。
#include <iostream>
using namespace std;
struct CNode
{
int data;
CNode *next;
};
void Josephus(int n, int k, int m)
{
CNode *head, *currPtr, *prevPtr, *newNode;
head = new CNode;
head->data = 1;
head->next = head;//循环链表的特色
currPtr = head;
for(int i=2; i<=n; ++i)
{
newNode = new CNode;
newNode->data = i;
newNode->next = head;
currPtr->next = newNode;
currPtr = newNode;
}//【另】循环链表的删除和插入不涉及表首和表尾的特殊情况的考虑,因此,一般不会出现在试题中。
currPtr = head;//将currPtr指向表头
while(--k)//k>=1,循环k-1次为的是找到第k个结点
{
//当m=1时,j=m-1=0,于是下面循环的子循环不执行,于是后面的程序便用到这里的prevPtr了。
prevPtr = currPtr;//不能缺少这条语句,否则m=1时运行失败!
currPtr = currPtr->next;//循环结束后currPtr指向第k个人
}
while(n--)//n>=1,每次循环减少一个,一共需要n次循环
{
int j = m-1;
while(j--)//从~m共需前进m-1步
{
prevPtr = currPtr;
currPtr = currPtr->next;
}
prevPtr->next = currPtr->next;
cout << currPtr->data << " is deleted\n";
delete currPtr;
currPtr = prevPtr->next;
}
}
int main()
{
Josephus(13, 4, 1);//m=1意味着从第k(这里为)个人开始,依次进行删除
return 0;
}
4. 队列
(1)实现一个队列
#include <iostream>
using namespace std;
//通过链表来实现队列
struct Node
{
int data;
Node *next;
};
struct LQueue//代表linked queue
{
Node *front;
Node *rear;
};
int main()
{
//一个队列本身只存放两个数据,即队首front和队尾rear,至于队列的具体实现过程则交给链表去执行,
//因此,构造队列只比构造单链表多了一步定义front和rear的过程。
LQueue *lq = new LQueue;
Node *newNode, *currPtr;
newNode = new Node;
newNode->data = 0;
newNode->next = NULL;
lq->front = newNode; //保存队首地址
currPtr = newNode;
for(int i=1; i<10; ++i)
{
newNode = new Node;
newNode->data = i;
currPtr->next = newNode;
currPtr = newNode;
}
currPtr->next = NULL; //这句仍不能丢掉,因为构造队列的具体细节其实就是构造单链表,只不过接口不同而已。
lq->rear = currPtr; //保存队尾地址
for(currPtr=lq->front; currPtr!=NULL; currPtr=currPtr->next)
{
cout << currPtr->data << " ";
}
cout << endl;
}
(2)队列的入队
#include <iostream>
using namespace std;
//通过链表来实现队列
struct Node
{
int data;
Node *next;
};
struct LQueue//代表linked queue
{
Node *front;
Node *rear;
};
//队列的入队只允许在队尾插入新结点,因此不用考虑其他情况,这比单链表和双链表简单
void insertNode(LQueue *&lq, const int item)
{
Node *newNode = new Node;
newNode->data = item;
newNode->next = NULL;
//队列只允许在队末插入新结点
if(NULL == lq->rear)
{
lq->front = newNode;
lq->rear = newNode;
}
else
{
lq->rear->next = newNode;
lq->rear = newNode;
}
}
(3)队列的出队
#include <iostream>
using namespace std;
struct Node
{
int data;
Node *next;
};
struct LQueue
{
Node *front;
Node *rear;
};
void deleteNode(LQueue *&lq)
{
if(NULL == lq->front)
return;
else
{
Node *temp;
temp = lq->front;
lq->front = lq->front->next;
delete temp;
}
}
5. 栈
(1)实现一个栈
#include <iostream>
using namespace std;
//通过链表来实现栈
struct Node
{
int data;
Node *next;
};
struct LStack//代表linked stack
{
Node *top;//栈顶
};
int main()
{
LStack *ls = new LStack;
//这里也可以直接构造LStack对象(LStack ls;),但是为了与前面相一致,以使得pushNode函数
//和popNode函数的参数都为指针形式(方便记忆),这里仍用动态分配的方式
Node *newNode = new Node;
newNode->data = 0;
newNode->next = NULL;
ls->top = newNode;
for(int i=1; i<10; ++i)
{
newNode = new Node;
newNode->data = i;
newNode->next = ls->top;
ls->top = newNode;
}
//下面的输出不能称为是栈的打印,因为这是链表的接口用法,对栈而言,它应该只能用popNode()函数
//这里仅仅是为了显示一下栈中的数据而已。
//真正的栈的打印见后面。
for(Node *currPtr=ls->top; currPtr!=NULL; currPtr=currPtr->next)
{
cout << currPtr->data << " ";
}
cout << endl;//输出:8 7 6 5 4 3 2 1
return 0;
}
(2)栈的入栈
struct Node
{
int data;
Node *next;
};
struct LStack//代表linked stack
{
Node *top;//栈顶
};
void pushNode(LStack *&ls, int item)
{
Node *newNode = new Node;
newNode->data = item;
newNode->next = ls->top;
ls->top = newNode;
//【注】栈的新结点与原栈顶的指向不能弄反了,否则不能按照“后进先出”的规则出栈。
}
(3)栈的出栈
struct Node
{
int data;
Node *next;
};
struct LStack//代表linked stack
{
Node *top;//栈顶
};
//出栈函数不应该返回出栈结点的值(因为STL中的pop函数也没有返回值)
void popNode(LStack *&ls)
{
if(NULL == ls->top)//栈为空时
{
cerr << "the stack have been empty\n";
exit(1);
}
else
{
Node *temp = ls->top;
ls->top = ls->top->next;
delete temp;
}
}
(4)栈的打印
void printStack(LStack *&ls)
{
while(ls->top != NULL)
{
cout << popNode(ls) << " ";
}
cout << endl;
//【注】栈的打印要体现栈本身的特性,故一定要要popNode()函数进行栈的打印!
//当打印完毕后,栈也就被清空了
}
(5)用两个栈实现一个队列的功能
这道题不像前面那些题那样,本题不要求自己编写一个队列,而是以栈为基本元素来实现队列的入队、出队这样的功能,也即,本题所述的“栈”是STL中的栈模板,故本题属于泛型编程的范畴。因此,不要将本题中的pop()成员函数与前面自定义的popNode()函数相混淆!
#include "stdafx.h"
#include <iostream>
#include <stack>
using namespace std;
//用s1提供入队操作,而用s2提供出队操作。
//【注】front()与deleteNode()的不同就如同stack的类成员函数中top()与pop()的不同一样,
//前者返回队首元素的引用,而后者令队首元素出队/删除
template <typename T>
struct SQueue//代表stack queue
{
stack<T> s1, s2;//对于类或结构体而言,成员变量放在成员函数前、后都是一样的。
void insertNode(T &item)
{
s1.push(item);
}
T front()
{
if(s2.empty())
{
while(!s1.empty())
{
s2.push(s1.top());
s1.pop();
//【注】不要将栈的类成员函数top()和pop()像混淆,前者是返回栈顶值的引用,而后者则
//是将栈顶结点删除/弹出,并且无返回值,所以不能上面两句合并为s2.push(s1.pop())
}//将s1的所有元素都压栈到s2中,以便下面返回s2的栈顶
}
if(!s2.empty())
return s2.top();
else
{
cerr << "stack have been empty!\n";
exit(1);
}
}
void deleteNode()
{
if(s2.empty())
{
while(!s1.empty())
{
s2.push(s1.top());
s1.pop();
}
}//将s1的所有元素都压栈到s2中,以便下面对s2进行pop操作
if(!s2.empty())
s2.pop();
}
};
int main()
{
SQueue<int> sq;
int i;
for(i=0; i<10; ++i)
sq.insertNode(i);
for(i=0; i<10; ++i)
{
cout << sq.front() << " ";
sq.deleteNode();
}
cout << endl; //输出:1 2 3 4 5 6 7 8 9
return 0;
}
6. 二叉搜索树
由于二叉树没有二叉搜索树那样的特性(对每一个结点,其左子树中的数据值都小于结点本身的数据值,而右子树中的数据值都大于或等于该结点的数据值),所以也就没有了像二叉搜索树那样的成员函数(如:插入结点、删除结点、搜索),这使得大部分关于二叉树的考察都是集中在二叉搜索树中的。
【注】如果不作特别说明的话,一般情况下对二叉树和二叉搜索树是不作区分的。
另外,二叉树的应用还有堆、AVL树。
下面重点讲述二叉搜索树:
【思想】二叉搜索树的递归过程其实就是一个扫描过程,在复制树(copyTree)和清除树(_clearTree)中用到的是后序扫描(LRN);而在打印树(_printTree)中用到的是(右)中序扫描(RNL);而在搜索树(_findTree)中用的是前序扫描(NLR)。之所以扫描方式不同,是为了更方便得完成各自的任务。
#include <stdafx.h>
#include <iostream>
using namespace std;
struct TreeNode
{
int data;
TreeNode *left;
TreeNode *right;
};
class BinSTree
{
private:
TreeNode *root;
private:
TreeNode *copyTree(TreeNode *node);//用在复制构造函数和赋值操作符中
//【注】以下两个私有成员函数为了和相应的公有成员函数相区别,分别在各自前面加了一个下划线,但是如果不加的话其实也正确,因为这属于函数重载(参数列表是不同的)
void _clearTree(TreeNode *node);//在clearTree和析构函数中会被用到
void _printTree(TreeNode *node, int level);//只能用在printTree函数中
TreeNode *_findNode(TreeNode *node, const int item)const;//只能用在findNode中
void _insertNode(TreeNode *&node, TreeNode *newNode);//只能用在insertNode中
public:
BinSTree():root(NULL){}//必须对root进行初始化
//由于二叉搜索树需要进行深度复制,故还得定义复制构造函数、析构函数和赋值操作符,但是鉴于这三个函数对二叉搜索树的构建、插入结点、删除结点、打印没有影响,故可以省略。
BinSTree(const BinSTree &tree);
~BinSTree(){_clearTree(root);}
BinSTree & operator=(const BinSTree &rhs);
TreeNode *findNode(const int item)const;
void insertNode(const int item);
void deleteNode(const int item);
//printTree与clearTree对一般二叉树也适用。
void printTree();
//在clearTree中调用_clearTree后,要将root清为NULL,否则root将成为悬浮指针。
void clearTree();
};
//复制树,私有成员函数
//用在复制构造函数和赋值操作符中
TreeNode *BinSTree::copyTree(TreeNode *node)
{
if(NULL == node)
return NULL;
TreeNode *newNode = new TreeNode;
newNode->left = copyTree(node->left);
newNode->right = copyTree(node->right);
newNode->data = node->data;
return newNode;
}
//删除树,私有成员函数
//用在析构函数、赋值操作符以及clearTree中,为了进行递归操作,需要在_clearTree中加一个参数表示当前子树的根节点
void BinSTree::_clearTree(TreeNode *node)
{
//按LRN(后序)的扫描顺序进行扫描并删除结点的。具体地,先从左子树的最下层依次向上进行删除,再从右子树的最下层依次向上进行删除。
if(node != NULL)
{
_clearTree(node->left);
_clearTree(node->right);
delete node;
}
}
//打印树,私有成员函数
//只能用在printTree函数中
//在打印二叉搜索树时,如果成员函数printTree没有参数,则很难使用递归来进行打印编程,因此,在该函数中添加了每次打印时的//子树的根节点node以及其所处的层次(这通过level个空格来控制)。
void BinSTree::_printTree(TreeNode *node, int level)
{
//终止条件
if(NULL == node)
return;
//递归步骤
//采用RNL的顺序进行横向打印
_printTree(node->right, level+1);//打印右边的结点
for(int i=0; i!=level; ++i)
cout << " ";//第level层的结点前加level个空格,以表明层次性
cout << node->data << endl;//打印当前结点
_printTree(node->left, level+1);//打印左边的结点
}
//打印二叉树/二叉搜索树
void BinSTree::printTree()
{
_printTree(root, 0);
}
//复制构造函数
BinSTree::BinSTree(const BinSTree &tree)
{
root = copyTree(tree.root);
}
//清除树并将根节点清为空指针
void BinSTree::clearTree()//要将清为NULL的node返回给调用函数
{
_clearTree(root);
//调用_clearTree后,要将root清为NULL,否则root将成为悬浮指针,
//从而影响到下一步的插入等操作。
root = NULL;
}
//赋值操作符
BinSTree & BinSTree::operator=(const BinSTree &rhs)
{
//避免自我复制
if(this == &rhs)
return *this;
clearTree();//清除当前树
root = copyTree(rhs.root);
return *this;
}
//搜索结点
//(1)非递归/迭代方法
TreeNode *BinSTree::findNode(const int item)const
{
TreeNode *currPtr = root;
while(currPtr != NULL)
{
if(item == currPtr->data)
break;
if(item < currPtr->data)
currPtr = currPtr->left;
else
currPtr = currPtr->right;
}
return currPtr;
//当currPtr!=NULL时循环结束,表明已经找到值为item的结点,否则,表明未找到。
}
//(2)递归方法
//私有成员函数
//只能用在findNode函数中,用来作findNode的递归部分
TreeNode *BinSTree::_findNode(TreeNode *node, const int item)const
{
if(NULL == node)//终止条件
return NULL;
else//递归步骤
{
//递归步骤用的是前序扫描(NLR)。
if(item == node->data)
return node;
if(item < node->data)
return _findNode(node->left, item);//注意不要落掉return
else
return _findNode(node->right, item);//注意不要落掉return
}
}
//搜索结点
//递归方法
TreeNode *BinSTree::findNode(const int item)const
{
return _findNode(root, item);
}
//插入结点
【思想】插入结点一定是在叶子结点上插入的(空树除外)。这是由二叉搜索树的性质决定的。可见,插入结点要比删除结点简单得多。
//(1)非递归/迭代方法
void BinSTree::insertNode(const int item)
{
TreeNode *currPtr=root, *parentPtr=NULL, *newNode;
while(currPtr != NULL)
{
parentPtr = currPtr;
if(item < currPtr->data)
currPtr = currPtr->left;
else
currPtr = currPtr->right;
}
//循环结束后,currPtr==NULL,而parentPtr一定为叶子结点
newNode = new TreeNode;
newNode->data = item;
newNode->left = NULL;
newNode->right = NULL;
if(NULL == parentPtr)
root = newNode;//特殊情况
else if(item < parentPtr->data)
parentPtr->left = newNode;
else
parentPtr->right = newNode;
}
//(2)递归方法
//私有成员函数
//只能用在insertNode中,遍历顺序同样为NLR
void BinSTree::_insertNode(TreeNode *&node, TreeNode *newNode)
{
if(NULL == node)
node = newNode; //因为node参数是用的引用,因此,可以通过修改形参已达到修改实参的目的(这时形参是实参的别名)
else if(newNode->data < node->data)
_insertNode(node->left, newNode);
else
_insertNode(node->right, newNode);
}
//插入结点
//递归方法
void BinSTree::insertNode(const int item)
{
TreeNode *newNode = new TreeNode;
newNode->data = item;
newNode->left = NULL;
newNode->right = NULL;
_insertNode(root, newNode);
}
//删除结点
【思想】
1、查找删除结点D:首先查找值等于item的结点D及其双亲结点P(P:parent, D:delete, R:replace);
2、查找替换结点R并将D的子树连接到R上:如果D可以找到,则再找D的替换结点R,这一点只与D的子树有关,而与D的上层结点都无关。该步骤分为三种情况:
(1)D无左子树:选择右子树的根结点作为R。
(2)D无右子树:选择左子树的根节点作为R。
另外,前两种情况有交叉,即D既无左子树又无右子树,这种情况包含在了前两种情况中,故无需单独考虑
(3)D既有左子树,又有右子树:选择D的左子树最右边的结点作为R。这又分为两种子情况:
① D的左孩子没有右子树,这时,R为D的左孩子;
② D的左孩子有右子树,易找到R,将R的左子树连接到R原来的双亲结点PofR上并作为PofR的右子树。
3、用R连接到D的父结点上:这时涉及到D与P相连接的问题。分为三种情况:
① P=NULL,即D为根结点,这时,令R作为根结点即可。
② D为P的左孩子,这时,令R作为P的左孩子。
③ D为P的右孩子,这时,令R作为P的右孩子。
4、删除D。
【注】全部过程与P的上层结点毫无关系,它影响的只是P及其下层的结构。
void BinSTree::deleteNode(const int item)
{
TreeNode *PNodePtr, *DNodePtr, *RNodePtr, *PofRNodePtr;
//P:parent, D:delete, R:replace,PofR:parent of R
//第一步:寻找D以及它的双亲结点P
for(PNodePtr=NULL,DNodePtr=root; DNodePtr!=NULL && item!=DNodePtr->data; )
{
PNodePtr = DNodePtr;
if(item < DNodePtr->data)
DNodePtr = DNodePtr->left;
else
DNodePtr = DNodePtr->right;
}
if(NULL == DNodePtr) //特殊情况,没找到D
return;
//第二步:查找R并将D的子树连接到R上
if(NULL == DNodePtr->left) //(1)D没有左子树,不用考虑将D的子树连接到R的问题
RNodePtr = DNodePtr->right;
else if(NULL == DNodePtr->right) //(2)D没有右子树不用考虑将D的子树连接到R的问题
RNodePtr = DNodePtr->left;
else //(3)D既有左子树又有右子树,需要考虑将D的子树连接到R的问题
{
//查找R
for(PofRNodePtr=DNodePtr,RNodePtr=DNodePtr->left; RNodePtr->right!=NULL; RNodePtr=RNodePtr->right)
{
PofRNodePtr = RNodePtr;
}
//将D的子树连接到R上,又分为两种情况:
if(PofRNodePtr == DNodePtr) //① D的左孩子没有右子树
RNodePtr->right = DNodePtr->right;
else //② D的左孩子有右子树
{
PofRNodePtr->right = RNodePtr->left;
RNodePtr->left = DNodePtr->left;
RNodePtr->right = DNodePtr->right;
}
}
//第三步:将R连接到D的父结点上
if(NULL == PNodePtr) //特殊情况,D即原树的根结点
root = RNodePtr;
else if(PNodePtr->left == DNodePtr)
PNodePtr->left = RNodePtr;
else
PNodePtr->right = RNodePtr;
//第四步:删除D
delete DNodePtr;
}
int main()
{
int a[10] = {5,2,6,3,9,4,1,8,10,7};
BinSTree bstree;
//二叉搜索树的构建
for(int i=0; i!=10; ++i)
{
bstree.insertNode(a[i]);
}
bstree.printTree();
//删除值为的结点
bstree.deleteNode(5);
bstree.printTree();
//插入值为的结点
bstree.insertNode(5);
bstree.printTree();
//清除二叉搜索树bstree
bstree.clearTree();
//在空树中插入值为的结点
bstree.insertNode(1);
bstree.printTree();
//复制构造函数
BinSTree bst2(bstree);
bst2.printTree();
//赋值操作符
BinSTree bst3 = bstree;
bst3.printTree();
}
7. 排序
(1)知识点
① 排序对象
一般意义上的排序可以是任意文件,而文件是由一组记录组成的。记录则由若干数据项/域组成。其中有一项可用来标识其所属的记录,该项称为关键项,而关键项的值称为关键字(Key)。
常见的排序对象有数组、链表、二叉树。
② 排序的稳定性
在待排序的文件中,若存在多个关键字相同的记录,经过排序后这些具有相同关键字的记录之间的相对次序保持不变,则称这种排序方法是稳定的。若具有相同关键字的记录之间的相对次序发生变化,则称这种排序方法是不稳定的。
冒泡排序法、归并排序法是稳定的排序法。而其他的排序法是不稳定的。
③ 排序方法的分类
i
按排序性能好坏依次为:插入排序法(也称直接插入排序法)、选择排序法、交换排序法、冒泡排序法
另外,当文件接近正序时,直接插入排序法和冒泡排序法均最佳。
【规律】直接插入排序法的双层循环为外前内后(内层循环索引的初始值为外层索引变量i);而选择排序法和交换排序法为内外均前(内层循环索引的初始值为i+1),且选择排序法由于smallPos的引入而使得其不用再内层循环中进行交换操作,进而优于交换排序法(除此之外,二者几乎相同);而冒泡排序法为外后内前(内层循环索引的终止值为外层索引变量i-1)。
ii
希尔(Shell)排序法
iii
按排序性能好坏依次为:快速排序法、堆排序法、归并排序法、树排序法、竞赛排序法
(2)插入排序法
//对“插入”的理解:如果有个元素大于temp(新插入元素的值),则将这个元素及其后的元素依次后移,然后将temp
//插入到最后移位的那个元素留下的空缺位处,这便是“插入”之所在。
void insertSort(int a[], int n)
{
int i, j;//用于循环的索引
int temp;//插入排序法所特有,也即为插入排序法的主线
for(i=1; i<n; ++i) //外前内后
{
temp = a[i];
//在未插入新元素之前,原来的元素是已排好序的,故如果出现a[j-1]<temp时,则a[j-1]之前的元素也
//一定小于temp,于是,循环可以就此终止了。
//【注】下面的循环判断条件一定是与temp进行比较,如果比temp大则后移。
for(j=i; j>0 && temp<a[j-1]; --j) //外前内后,且内层循环索引的初始值为外层索引变量i
a[j] = a[j-1];//右移表中元素
a[j] = temp;//这时的a[j]恰好是最后一次右移时的a[j-1],这位每次循环后都要执行“--j”。
}
}
(3)选择排序法
void selectSort(int a[], int n)
{
int smallPos;//选择排序法所特有的,也是选择排序法的主线
int i, j;//用于排序
for(i=0; i<n-1; ++i) //内外均前
{
smallPos = i;
for(j=i+1; j<n; ++j) //内外均前,且内层循环索引的初始值为i+1
if(a[j]<a[smallPos])
smallPos = j;
swap(a[i], a[smallPos]); //C++标准库中的swap函数,不需头文件
}
}
(4)交换排序法
//交换排序法是最一般的排序法:
void exchangeSort(int a[], int n)
{
int i, j;
for(i=0; i<n-1; ++i) //内外均前
for(j=i+1; j<n; ++j) //内外均前,且内层循环索引的初始值为i+1
if(a[j] < a[i])
swap(a[j], a[i]); //C++标准库中的swap函数,不需头文件
}
(5)冒泡排序法
void bubbleSort(int a[], int n)
{
int lastExchangePos;//冒泡排序法所特有的,也是冒泡排序法的主线
int i, j;//用于排序
for(i=n-1; i>0; i=lastExchangePos) //外后内前
{
lastExchangePos = 0; //防止内层没有发生交换时lastExchangePos不改变以至变为死循环
for(j=0; j<i; ++j) //外后内前,且内层循环索引的终止值为外层索引变量i-1
if(a[j+1]<a[j])
{
swap(a[j+1], a[j]); //C++标准库中的swap函数,不需头文件
lastExchangePos = j;
//助记:如果内层循环完全执行,则最后依次交换后j=i-1,
//而lastExchangePos也变为i-1,即仅前移一步,这是合理的
}
}
}
(6)希尔(Shell)排序法
希尔排序法是插入排序法的一种,因D.L.Shell于1959年提出而得名。希尔排序法的时间性能优于直接插入排序法,它是
希尔排序法的性能优于直接插入排序法的原因:
在希尔排序开始时,增量较大,分组(第二层循环)较多,每组的记录数目少,故各组内直接插入较快。后来增量d逐渐缩小,分组数逐渐减少,而各组的记录数目逐渐增多。但由于已经按d-1的距离排过序,使得文件较接近于有序状态,所以新的一趟排序过程也较快(因为当文件接近正序时,直接插入排序法和冒泡排序法均最佳)。因此,希尔排序在效率上较直接插入排序法有较大的改进。
void shellSort(int *a, int n)
{
int i, j;//用于循环的索引
int temp, d;
//temp为插入排序法所特有,也即插入排序法的主线;而d代表distance,即每次循环时的增量或需要互换的两元素的距离,d为 //希尔排序法的主线
for(d=n/2; d>0; d/=2) //控制增量
{
//这个实际上就是直接插入排序法的扩展版本,因为当d=1时,控制增量循环内的这两层循环完全就是直接插入法的循环,
//这再次印证了希尔排序法属于插入排序法这一结论。
for(i=d; i<n; i++)
//这里i为逐一递增,只有这样才能保证每次外层循环所有的元素都可能进行互换,从而更快地完成排序
{
temp = a[i];
for(j=i; j>0 && temp<a[j-d]; j-=d) //【注】循环判断条件一定是与temp进行比较,如果比temp大则后移。
a[j] = a[j-d];
a[j] = temp;
}
}
}
(7)快速排序法
① 方法一:
【注】方法一是方法二的改进版,因此只需用方法一即可,方法二仅供参考。
方法一的最坏情况:当原数组为升序或降序时,复杂度为
void quickSort(int a[], int low, int high) //由于要进行递归编程,故要多设一个参数(由n变为low和high)用作递归控制
{
int i, j;//i和j分别是正向扫描和逆向扫描的索引
if(low < high)
{
//先扫描后判断
do
{
//i只需扫描到high-1、j只需扫描到low+1即可,因为如果只有两个元素时,第一个循环就不用执行,而直接执行第二//个循环中的if语句来决定是否需要互换即可(当前一元素大于后一元素时,执行--j,进而j<i或者说j=low,从而在//外层循环后的swap语句相当于没执行;反之,不执行--j,于是在循环内的swap执行后变为顺序了,此后,还要进行//一次外层循环,这时便又回到了第一种情况)。
//【注】将数组a[low]的第一个元素视为中心点(由于数组排列的随机性,将任何元素视为起始中心点其实都一样)
for(i=low+1; i<high; ++i)
if(a[i] > a[low])
break;
for(j=high; j>low; --j)
if(a[j] <= a[low])
break;
if(i < j)
swap(a[i], a[j]); //保证了比pivot小的存放在左边,比pivot大的存放在右边
}
while(i < j);
//将中心点移到正确的位置(即j所在的位置)
swap(a[j], a[low]);
//因为在交换前,a[low+1]~a[j-1]的元素都比a[low]小,a[j+1]~a[high]的元素都比a[low]大,所以将原数组分为两部分,
//而上一步得到的中心点不再参加递归,故低端子表范围为low~j-1;高端子表为j+1~high,并将上一步未交换时的a[j]作 //为新的中心值a[low]
quickSort(a, low, j-1);
quickSort(a, j+1, high);
}
}
② 方法二:
【注】方法一是方法二的改进版,因此只需用方法一即可,方法二仅供参考。
方法二的最坏情况:当中心值总是落在一个单元素子表中,而其余元素都在另一个子表中,复杂度为
//由于要进行递归编程,故要多设一个参数(由n变为low和high)用作递归控制
void quickSort(int a[], int low, int high)
{
int mid, pivot;//mid:中心元素的索引;pivot:['pivət],中心点,重点,中心元素的值
int scanUp, scanDown;//用于扫描
//特例:最多只有一个元素
if(high-low <= 0)
return;
//终止条件
if(high-low == 1)//只有两个元素
{
if(a[high]<a[low])
swap(a[high], a[low]);//C++标准库中的swap函数,不需头文件
return;
}
//初始中心元素的索引和值
mid = (low+high)/2;
pivot = a[mid];
swap(a[low], a[mid]);//C++标准库中的swap函数,不需头文件
//scanUp与scanUp的初始值
scanUp = low+1;
scanDown = high;
//先扫描后判断
do
{
while(scanUp<=scanDown && a[scanUp]<=pivot)
++scanUp;
while(scanUp<=scanDown && a[scanDown]>pivot)
--scanDown;
//【注】经过前面的两轮扫描,不可能再有scanUp==scanDown的情况出现了
if(scanUp<scanDown)
swap(a[scanUp], a[scanDown]);//C++标准库中的swap函数,不需头文件
}
while(scanUp<scanDown);
//当前面的扫描循环执行完毕后,scanUp刚好到了高端子表中(即高端子表的第一个元素),而
//scanDown处在中心位置,并将中心点移到中心位置处。
a[low] = a[scanDown];
a[scanDown] = pivot;
//上一步得到的中心点不再参加递归,故低端子表范围为low~scanDown-1;高端子表为scanDown+1~high
if(low<scanDown-1)
quickSort(a, low, scanDown-1);
if(scanDown+1<high)
quickSort(a, scanDown+1, high);
}
(8)堆排序法
堆是按由小到大(双亲值小于或等于其孩子值,称为最小堆(maximum heap))或由大到小(双亲值大于或等于其孩子值,称为最大堆(minimum heap))的顺序将一系列数据以完全二叉树的形式存放的一种非线性表。
该方法用到了二叉树的顺序(数组)表示方式(也称为基于数组的二叉树)。堆是结点间具有层次次序关系和抽象表机构的完全二叉树。
对于每次插入和删除操作,堆仅需扫描从根到树的末端的短路径即可恢复其顺序(堆化)。
堆排序原理:将未排序的数组通过数组堆化构造函数生成堆(最小堆)的形式,然后通过deleteHeap()函数将堆化数组排序成由大到小的顺序。
#include <stdafx.h>
#include <iostream>
using namespace std;
class Heap
{
private:
int *hList;//hList指向一个数组,代表heap list
int maxSize, size;//maxSize是堆中可存放的元素的最大个数,size是当前堆大小
void filterUp(int i);//用在insertHeap函数中
void filterDown(int i);//用在deleteHeap和堆化数组的构造函数中
public:
Heap(int a[], int n);//通过数组来构造堆
//由于在堆排序中不会用到下面的四个成员函数,因此这里就不做定义了
/*Heap(int maxSize);//构建空堆
Heap(const Heap &h);//复制构造函数
~Heap();
Heap & operator=(const Heap &h);*/
void insertHeap(const int item);//堆插入函数
int deleteHeap();//堆删除函数,返回最小值以便进行堆排序
};
//向上堆化函数
//用在insertHeap函数中,并非数组堆排序所必须的函数
//【注】向上堆化和向下堆化都是对数组进行堆化,差别仅在于新插入值位于表尾还是表首。
//由于堆插入时是将一个新结点加入到原堆的最后一个位置,于是要想调整顺序使其堆化,
//则必须向上进行位置调整,于是便有了“filterUp”。
void Heap::filterUp(int i)//【注】参数是要进行排序的元素的索引
{
int currPos, parentPos;//注意,这里currPos和parentPos是数组的索引,而非指针
//以下完全是直接插入排序法内层循环的思想
int temp = hList[i];//向上堆化涉及到了直接插入排序法,而temp又是后者所特有的
//向下堆化是用currPos来控制边界的,因为当currPos为0时则不会执行下面的语句从而就不会出现hList[parentPos]以致错误
//在下面的循环控制条件中,当currPos为零时,可以看作在空表中插入,故必须考虑
for(currPos=i,parentPos=(i-1)/2; currPos!=0 && temp<hList[parentPos];
currPos=parentPos,parentPos=(parentPos-1)/2) //直接插入排序法:外前内后,而这里是内层循环的思想,故从后向前
hList[currPos] = hList[parentPos];
hList[currPos] = temp;
}
//堆插入函数,并非数组堆排序所必须的函数
void Heap::insertHeap(const int item)
{
if(size == maxSize)
{
cerr << "heap full" << endl;
exit(1);
}
hList[size] = item;//开始时,将item插入到堆的末尾
filterUp(size++);//对新插入的结点进行堆化,参数是新插入结点的索引
}
//向下堆化函数
//用在deleteHeap和数组堆化构造函数,是数组堆排序所必须的函数
//【注】向上堆化和向下堆化都是对数组进行堆化,差别仅在于新插入值位于表尾还是表首。
void Heap::filterDown(int i)//【注】参数是要删除的元素的索引
{
int currPos, childPos;
int temp = hList[i];//向下堆化涉及到了直接插入排序法,而temp又是后者所特有的
//以下除了比较两个孩子大小外,其余完全是直接插入排序法的思想
//由于是在表首插入,因此,这里的循环起始值与前面的直接插入排序法的内循环起始插入值不同,这里是从前向后进行循环
for(currPos=i,childPos=2*i+1; childPos<size; currPos=childPos,childPos=2*childPos+1)
//向下堆化是用childPos来控制边界的,因为循环中涉及到了hList[childPos]
{
//千万不要漏掉“childPos+1<size”这个条件,以保证右孩子不会出界,否则程序会出错
//向下堆化是用孩子结点来控制边界的,但这里还涉及到了右孩子,故以childPos+1(最先接触边界)来控制边界
if(childPos+1<size && hList[childPos]>hList[childPos+1])
++childPos;//置childPos为较小孩子的下标
if(temp > hlist[childPos]) //由于在for语句中childPos还未确定,故不能放在for中作为条件判断
hlist[currPos] = hlist[childPos]; //由于是表首插入,故进行的是前移(与直接插入排序法相区别)
else
break; //由于之前childPos未确定而导致循环控制条件被滞后
}
hList[currPos] = temp;
}
//堆删除函数,是数组堆排序所必须的函数
int Heap::deleteHeap()
{
if(0 == size)
{
cerr << "heap empty" << endl;
exit(1);
}
int item = hList[0];//要返回的值,也即当前堆的根元素
hList[0] = hList[--size];//将堆的末元素来替代根元素,并将size减一
filterDown(0);//对新堆进行堆化
return item;
}
//数组堆化构造函数,是数组堆排序所必须的函数
Heap::Heap(int a[], int n)
{
if(n <= 0)
{
cerr << "bad size" << endl;
exit(1);
}
int currPos;
maxSize = n;
size = n;
hList = a;
//将currPos置为最大双亲结点的下标,然后依次进行向下堆化,直到currPos为0时进行最后一次堆化
for(currPos=(size-2)/2; currPos>=0; --currPos)
filterDown(currPos); //可见,堆排序其实只需filterDown函数而不需insertSort函数
}
//堆排序函数,是数组堆排序所必须的函数
void heapSort(int a[], int n)
{
Heap h(a, n);//先对数组a进行堆化
for(int i=n-1; i>0; --i) //i=0时就不用再排序了,因为最后得到的a[0]一定是最大值
a[i] = h.deleteHeap();//依次删除堆中的根元素,从而达到排序的目的
//这是因为堆每执行依次deleteHeap操作后,就将size减一,于是以后不再对该元素进行操作,
//因此,可以将以删除的元素依次从数组的末端向前逆向存储,这样到最后,数组便被排好序了,
//而且不占用其他数组。只不过数组最后是从大到小的(对最小堆而言)
}
int main()
{
int arr[10] = {5,0,6,9,3,4,7,8,2,1};
heapSort(arr, 10);
int i;
for(i=0; i<10; ++i)
{
cout << arr[i] << " ";
}
cout << endl;
}
(9)归并排序法
归并排序法是一种外部存储设备最常用的排序方法。这种排序法可以分为两个步骤,如下所示:
① 将排序数据先分成几个文件,其中每个文件的大小是可以加载到内存中,然后使用适当的内部排序法运行排序,最后将排序完成的数据写回文件。
② 将第一步中创建的文件两两合并成一个文件,等到全部的文件都归并成一个文件后,文件数据也就排好序了。
一般将归并排序法的函数名设为mergeSort,具体如下:
#include <stdafx.h>
#include <iostream>
using namespace std;
void merge(int a[], int left, int mid, int right)
{
int *tempArr = new int [right-left+1];
int i=left, j=mid+1, k=0; //分别为左子数组、右子数组、临时数组的索引
while(i<=mid && j<=right) //情况一:两个子数组均未合并完毕
if(a[i] <= a[j])
tempArr[k++] = a[i++];
else
tempArr[k++] = a[j++];
//【注】下面的两种情况不能同时发生
while(i <= mid) //情况二:后一个子数组已经合并完毕
tempArr[k++] = a[i++];
while(j <= right) //情况三:前一个子数组已经合并完毕
tempArr[k++] = a[j++];
for(i=0; i<k; i++) //这时k为合并后的数组的元素个数
a[left++] = tempArr[i];
delete [] tempArr;
}
void mergeSort(int a[], int left, int right)
{
if(right > left)
{
//先分后合
int mid = (left+right)/2;
mergeSort(a, left, mid);
mergeSort(a, mid+1, right);
merge(a, left, mid, right); //将已排好序的子数组进行合并
}
}
void mergeSort(int a[], int len)
{
mergeSort(a, 0, len-1);
return ;
}
int main()
{
int a[] = {9,8,7,6,5,4,3,2,1,0};
mergeSort(a, 10);
for(int i=0; i<10; i++)
cout << a[i] << " ";
cout << endl;
return 0;
}
(10)竞赛排序法
该方法用到了二叉树的顺序(数组)表示方式(也称为基于数组的二叉树),即用数组项存储数据以及标识结点的索引。
【注】结点a[i]的双亲索引为:(i-1)/2(i>0);而结点a[i]的左孩子的索引为2i+1,右孩子的索引为2i+2。
【原理】
二叉树的重要应用之一是作为抉择树(decision tree)。树中每个结点都代表一个具有两个可能去向的分支。这类应用的一个例子是用树存储参加一种单淘汰比赛的运动员的记录,直至有一个运动员赢得决赛。这便是竞赛排序法的“竞赛”之来由。
如果用竞赛排序法对N个表项进行排序,则需建立一棵基于数组的树,该N个表项作为叶子结点位于树的底层(第k层,其中
虽然竞赛排序法的比较次数为
【例】通过竞赛排序法,还可以很容易找到一个数组的最大值、最小值以及最大值和最小值,它们分别需要n-1、n-1和(n-1)+(n/2-1)次比较运算。对于前两种情况,可以用一般的遍历方法,也可以用竞赛排序法,最后结果都是n-1次比较运算。但是对于最后一种情况,通过竞赛排序法更易得出结果:在第一轮竞赛排序完毕后,有一半的元素是大的,另一半是小的,其中最大值只需在大的那一半元素中查找即可,而最小值只需在小的那一半元素中查找即可,并且两部分从此互相独立。假设查找最大值的次数包括第一轮竞赛所用的比较次数,于是查找最大值用了n-1次比较运算,那么查找最小值的次数从另外的n/2个元素开始进行竞赛排序,又用了n/2-1次比较运算,因此总共需要(n-1)+(n/2-1)次比较运算。
8. AVL树
AVL树得名于其发明者G.M.Adelson-Velsky和E.M.Landis。AVL树是一个各结点具有平衡高度的扩展的二叉搜索树。在AVL树中,任一结点的两个子树的高度差最多为1,AVL树的高度不会超过
【结论】AVL树既有二叉搜索树的搜索效率又可以避免二叉搜索树的最坏情况(退化树)出现。
AVL树的表示与二叉搜索树类似,其操作基本相同,但插入和删除方法除外,因为它们必须不断监控结点的左右子树的相对高度,这也正是AVL树的优势所在。
下面是AVL树的结点,其中,balanceFcator(平衡因子)的值必须是-1、0或1三者之一。
left |
data |
balanceFactor |
right |
struct AVLTreeNode
{
int data;
AVLTreeNode *left, *right;
int balanceFactor;
};
当然,也可以通过对TreeNode的继承来实现。
AVL树的插入函数和删除函数比较复杂,故略(详见课本p.564)。