数据结构是相互之间存在一种或多种特定关系的数据元素的集合
通常有四种基本结构:
(1)集合
(2)线性结构
(3)树形结构
(4)图状结构或网状结构
结构定义中的“关系”描述的是数据元素之间的逻辑关系,因此又被称为数据的逻辑结构
数据结构在计算机中的表示(又称映像)称为数据的物理结构,又称存储结构。存储结构又分为顺序存储结构和链式存储结构。
抽象数据类型(Abstract Data Type 简称ADT)是指一个数学模型以及定义在此数学模型上的一组操作。抽象数据类型需要通过固有数据类型(高级编程语言中已实现的数据类型)来实现。抽象数据类型是与表示无关的数据类型,是一个数据模型及定义在该模型上的一组运算。对一个抽象数据类型进行定义时,必须给出它的名字及各运算的运算符名,即函数名,并且规定这些函数的参数性质。一旦定义了一个抽象数据类型及具体实现,程序设计中就可以像使用基本数据类型那样,十分方便地使用抽象数据类型。
算法是在有限步骤内求解某一问题所使用的一组定义明确的规则。通俗点说,就是计算解题的过程。在这个过程中,无论是形成解题思路还是编写程序,都是在实施某种算法。前者是推理实现的算法,后者是操作实现的算法。
一个算法应该具有以下五个重要的特征:
1、有穷性: 一个算法必须保证执行有限步之后结束;
2、确切性: 算法的每一步骤必须有确切的定义;
3、输入:一个算法有0个或多个输入,以刻画运算对象的初始情况,所谓0个输入是指算法本身定除了初始条件;
4、输出:一个算法有一个或多个输出,以反映对输入数据加工后的结果。没有输出的算法是毫无意义的;
5、可行性: 算法原则上能够精确地运行,而且人们用笔和纸做有限次运算后即可完成。
一般情况下,算法的基本操作重复执行的次数是模块n的某一个函数f(n),因此,算法的时间复杂度记做:T(n)=O(f(n))
分析:随着模块n的增大,算法执行的时间的增长率和 f(n) 的增长率成正比,所以 f(n) 越小,算法的时间复杂度越低,算法的效率越高。
2. 在计算时间复杂度的时候,先找出算法的基本操作,然后根据相应的各语句确定它的执行次数,再找出 T(n) 的同数量级(它的同数量级有以下:1,log(2)n,n,n log(2)n ,n的平方,n的三次方,2的n次方,n!),找出后,f(n) = 该数量级,若 T(n)/f(n) 求极限可得到一常数c,则时间复杂度T(n) = O(f(n))
空间复杂度(Space Complexity):
1:空间复杂度是对一个算法在运行过程中临时占用存储空间大小的量度;
2:一个算法在计算机上占用的内存包括:程序代码所占用的空间,输入输出数据所占用的空间,辅助变量所占用的空间这三个方面,程序代码所占用的空间取决于算法本身的长短,输入输出数据所占用的空间取决于要解决的问题,是通过参数表调用函数传递而来,只有辅助变量是算法运行过程中临时占用的存储空间,与空间复杂度相关;
3:通常来说,只要算法不涉及到动态分配的空间,以及递归、栈所需的空间,空间复杂度通常为0(1);
4: 对于一个算法,其时间复杂度和空间复杂度往往是相互影响的。当追求一个较好的时间复杂度时,可能会使空间复杂度的性能变差,即可能导致占用较多的存储空间;反之,求一个较好的空间复杂度时,可能会使时间复杂度的性能变差,即可能导致占用较长的运行时间。另外,算法的所有性能之间都存在着或多或少的相互影响。因此,当设计一个算法(特别是大型算法)时,要综合考虑算法的各项性能,算法的使用频率,算法处理的数据量的大小,算法描述语言的特性,算法运行的机器系统环境等各方面因素,才能够设计出比较好的算法。
线性表是最基本、最简单、也是最常用的一种数据结构。
线性表中数据元素之间的关系是一对一的关系,即除了第一个和最后一个数据元素之外,其它数据元素都是首尾相接的(注意,这句话只适用大部分线性表,而不是全部。比如,循环链表逻辑层次上也是一种线性表(存储层次上属于链式存储),但是把最后一个数据元素的尾指针指向了首位结点)。
线性表线性表一般包含如下几种操作:
线性表的操作包括如下几种
(1) InitList(& L)
//构造一个空的线性表
(2) DestroyList(& L)
//线性表存在了,消耗一个线性表
(3) ClearList(&L )
//清空线性表中的内容
(4) ListEmpty(L)
//判断线性表是否为空
(5) ListLength(L)
//返回线性表的长度
(6) GetElem(L,i,& e)
//返回线性表i位置上的元素值,通过e返回
(7) PriorElem(L,cur_e,&pre_e)
//如果cur_e是线性表中的元素,而且不是第一个,那么我们就可以返回该元素前一个元素的值
(8) NextElem(L,cur_e,&next_e)
//如果cur_e是线性表中的元素,而且不是最后一个,就返回它下一个元素的值
(9) Listinsert(&L,i,e)
//如果线性表存在了,而且i符合条件,则在i位置插入一个元素
(10)ListDelete(&L,i)
//删除i位置上的元素
(11) ListDelete_data(&L,e,order)
//删除指定的元素e,order决定是删除一个,还是全部。
(12) Connect_two_List(L_a,L_b,& L_c)
//连接两个线性表,除去重复的内容
(13)print(L)
//打印线性表
线性表的顺序表示实现–表示的格式
我们在之后的那些操作的实现中,我们都是一直使用这些内容,作为我们通过线性表顺序结构表示实现线性表操作的一些基础。
//扩充容量的步伐
#define sizestep 10
//开始的容量的大小
#define startsize 100
//为了便于扩展,我们这里使用类型别名,
//我们使用最简单的int型作为一个范例
typedef int Elemtype;
struct List {
//数据
Elemtype * data;
//长度
int length;
//初始容量
int size;
};
线性表顺序表示实现–initList函数
因为顺序表示就是对数组进行一些操作,我们在第四点已经给出了我们顺序表示线性表的一个结构体,我们要创建一个空表(没有元素的表),我们就是让表的长度为0,另外,还要对data进行堆内存的分配和初始容量的初始化。
//创建一个空的线性表
void InitList(List & newList) {
//初始容量为startsize
newList.size = startsize;
//首先开辟空间
newList.data = new Elemtype[newList.size];
//空表,长度是0
newList.length = 0;
}
线性表顺序表示实现–DestroyList函数
//线性表存在了,现在要销毁线性表
void DestroyList(List & newList) {
newList.length = 0;
newList.data = 0;
//一定要先释放堆内存
delete[] newList.data;
//没此释放堆内存后,并将对应的指针赋予NULL是一个良好的习惯
newList.data = NULL;
}
线性表顺序表示实现–ClearList函数
//线性表存在了,但是现在想清空整个线性表
void ClearList(List & newList) {
newList.length = 0;
//一定要先释放堆内存
delete[] newList.data;
//没此释放堆内存后,并将对应的指针赋予NULL是一个良好的习惯
newList.data = NULL;
//重新为存放元素的变量开辟一个新的堆内存
newList.data = new Elemtype[newList.size];
}
8、线性表顺序表示实现–ListEmpty函数
//判读线性表是否为空
bool ListEmpty(List newList) {
return newList.length;
}
线性表顺序表示实现–ListLength函数
//返回线性表的长度
int ListLength(List newList) {
return newList.length;
}
线性表顺序表示实现–GetElem函数
//返回线性表上某个位置上的元素的值,记住我们的位置是从1开始计算的。
void GetElem(List newList, int i, Elemtype &e) {
if (ListEmpty(newList)) {
cout << "当前线性表是空表" << endl;
return;
}
if (i<1 || i>newList.length) {
cout << "当前位置超出了线性表的范围" << endl;
return;
}
e = newList.data[i - 1];
}
我们需要考虑这个函数的时间复杂度:因为我们是通过下标进行查找对应的元素的,所以它是时间复杂度为:O(1),这个和我们后边说的链式结构的查找比起来,是占了很大优势的
11、线性表顺序表示实现–PriorElem函数
我们在执行这个函数的第一步就是判断我们的那个元素在不在线性表中且不是第一个元素,这样我们就可以直接返回该元素的前一个元素的值。
//判读元素的位置的函数
//我们这里直接返回该元素的下标
int LocationElem(List newList, Elemtype e){
int i;
for (i = 0; i < newList.length; i++) {
if (newList.data[i] == e) {
return i;
}
}
return -1;
}
这个函数的时间复杂为:假定我们有n个元素,那么它的查找时间复杂为O(n),但是因为我们使用的是顺序结构,所以我们可以很方便的使用其他可以减低时间复杂的查找算法,例如二分查找,它的时间复杂度为:O(logn)
//获取前驱的元素
void PriorElem(List newList, Elemtype cur_e, Elemtype & pre_e) {
int location = 0;
location = LocationElem(newList, cur_e);
//如果Location是-1,说明cur_e不在线性表中
if (location == -1) {
cout <
}
//如果Location是0,说明cur_e在线性表第一个位置,没有前一个元素
if (location == 0)
{
cout << cur_e << "是线性表的第一个元素,没有前驱" << endl;
return;
}
pre_e = newList.data[location - 1];
}
这个函数的时间复杂主要是受LocationElem函数的影响,
线性表顺序表示实现–NextElem函数
这个函数和前面的函数是一样的,我们只要修改一个位置的参数就可以了,那就是判断第一个元素的变为判断最后一个元素
//获取后驱元素
void NextElem(List newList, Elemtype cur_e, Elemtype & next_e) {
int location = 0;
location = LocationElem(newList, cur_e);
//如果Location是-1,说明cur_e不在线性表中
if (location == -1) {
cout << cur_e << "不在线性表中" << endl;
return;
}
//如果Location是0,说明cur_e在线性表最后一个位置,没有后一个元素
if (location == newList.length-1)
{
cout << cur_e << "是线性表的最后一个元素,没有后驱" << endl;
return;
}
next_e = newList.data[location - 1];
}
这个函数的时间复杂为
线性表顺序表示实现–Listinsert函数
//向线性表中插入一个元素,这里我们需要判断插入位置是否合法
//除此之外,我们还需要检测元素的容量是否已经到达了最大值
void Listinsert(List & newList, int i, Elemtype e) {
//插入的位置不合法
if (i<1 || i>newList.length+1) {
cout << "请检查插入位置是否正确" << endl;
return;
}
int j = 0;
//此时达到了线性表的最大容量,我们需要重新为线性表分配新的内存。
if (newList.length == newList.size) {
//先保存之前的内容。
Elemtype * p =new Elemtype[newList.length];
for (j = 0; j < newList.length; j++) {
p[j] = newList.data[j];
}
//扩大容量
newList.size += sizestep;
delete[] newList.data;
//重新分配内存
newList.data = new Elemtype[newList.size];
//恢复之前内容
for (j = 0; j < newList.length; j++) {
newList.data[j] = p[j];
}
}
//插入内容
for (int k = newList.length; k >i-1; k-- ){
newList.data[k]=newList.data[k-1];
}
newList.data[i - 1] = e;
++newList.length;
}
线性表的顺序结构表示的时候,它的最大的缺点就是在插入和删除的时候,需要移动大量的元素,此时,我们插入一个元素的时间复杂为:O(n),时间复杂度虽然是线性的,但是由于它需要移动大量的元素,这也就早造成了它的时间效率的比较低的
线性表顺序表示实现–Listdelete函数
//线性表删除一个元素,我们需要判断删除的位置是否合法
void Listdelete(List & newList, int i) {
//删除的位置不合法
if (i<1 || i>newList.length) {
cout << "请检查插入位置是否正确" << endl;
return;
}
for (int j = i - 1; j < newList.length; j++) {
newList.data[j] = newList.data[j + 1];
}
--newList.length;
}
线性表的删除和插入是差不多的意思,都是要对数组中的元素进行移动。
线性表顺序表示实现–Listdelete_data函数
//按照元素的值,来删除对应元素的内容,
//这个时候我们通过传个参数,来决定我们是删除第一个该元素,
//0,删除一个,1,删除所有
//还是把所有的元素都给删除了
//如果不存在该元素,就直接返回
void Listdelete_data(List & newList, Elemtype e,int order) {
int flag=0;
for (int i = 0; i < newList.length; i++) {
if (newList.data[i] == e) {
flag=1;
//删除对应的位置上的元素,而且i也要减少一个
Listdelete(newList, i + 1);
--i;
if (order == 0) {
return;
}
}
}
if(flag==1)
return ;
cout << e << "不在线性表中" << endl;
}
线性表顺序结构表示实现–Connect_two_List函数
当我们要进行两个线性表的链接的时候,我们最好是希望这两个链表是有序的。
//连接两个线性表
void Connect_two_List(List a, List b, List & c) {
//对c进行一些数据初始化
c.length = c.size = a.length + b.length;
c.data = new Elemtype[c.size];
//这里采用指针的方式进行数据的移动,我们先把a和b数据第一个和最后一个元素的位置找出来
int i = 0;
int j = 0;
int k = 0;
while (i <= a.length-1 && j<= b.length-1) {
if (a.data[i] < b.data[j])
{
c.data[k++] = a.data[i++];
}
else if(a.data[i] > b.data[j]) c.data[k++] = b.data[j++];
else {
c.data[k++] = b.data[j++]; i++; --c.length;
}
}
while (i <= a.length - 1)
{
c.data[k++] = a.data[i++];
}
while (j <= b.length - 1)
{
c.data[k++] = b.data[j++];
}
}
我们通过分析我们连接函数的代码,我们可以发现,我们将两个元素组合在一起的时候的时间复杂为O(a.lenght+b.lenght).
17、线性表顺序结构表示实现–print函数
void print(List & L) {
for (int i = 0; i < L.length; i++) {
cout << L.data[i] << " ";
}
cout << endl;
}
是一种常见的基础数据结构,结构体指针在这里得到了充分的利用。链表可以动态的进行存储分配,也就是说,链表是一个功能极为强大的数组,他可以在节点中定义多种数据类型,还可以根据需要随意增添,删除,插入节点。链表都有一个头指针,一般以head来表示,存放的是一个地址。链表中的节点分为两类,头结点和一般节点,头结点是没有数据域的。链表中每个节点都分为两部分,一个数据域,一个是指针域。说到这里你应该就明白了,链表就如同车链子一样,head指向第一个元素:第一个元素又指向第二个元素;……,直到最后一个元素,该元素不再指向其它元素,它称为“表尾”,它的地址部分放一个“NULL”(表示“空地址”),链表到此结束。
作为有强大功能的链表,对他的操作当然有许多,比如:链表的创建,修改,删除,插入,输出,排序,反序,清空链表的元素,求链表的长度等等。
初学链表,一般从单向链表开始
--->NULL
head
这是一个空链表。
---->[p1]---->[p2]...---->[pn]---->[NULL]
head p1->next p2->next pn->next
有n个节点的链表。
创建链表
typedef struct student{
int score;
struct student *next;
} LinkList;
一般创建链表我们都用typedef struct,因为这样定义结构体变量时,我们就可以直接可以用LinkList *a;定义结构体类型变量了。
初始化一个链表,n为链表节点个数。
LinkList *creat(int n){
LinkList *head, *node, *end;//定义头节点,普通节点,尾部节点;
head = (LinkList*)malloc(sizeof(LinkList));//分配地址
end = head; //若是空链表则头尾节点一样
for (int i = 0; i < n; i++) {
node = (LinkList*)malloc(sizeof(LinkList));
scanf("%d", &node->score);
end->next = node;
end = node;
}
end->next = NULL;//结束创建
return head;
}
修改链表节点值
修改链表节点值很简单。下面是一个传入链表和要修改的节点,来修改值的函数。
void change(LinkList *list,int n) {//n为第n个节点
LinkList *t = list;
int i = 0;
while (i < n && t != NULL) {
t = t->next;
i++;
}
if (t != NULL) {
puts("输入要修改的值");
scanf("%d", &t->score);
}
else {
puts("节点不存在");
}
}
删除链表节点
删除链表的元素也就是把前节点的指针域越过要删除的节点指向下下个节点。即:p->next = q->next;然后放出q节点的空间,即free(q);
void delet(LinkList *list, int n) {
LinkList *t = list, *in;
int i = 0;
while (i < n && t != NULL) {
in = t;
t = t->next;
i++;
}
if (t != NULL) {
in->next = t->next;
free(t);
}
else {
puts("节点不存在");
}
}
插入链表节点
我们可以看出来,插入节点就是用插入前节点的指针域链接上插入节点的数据域,再把插入节点的指针域链接上插入后节点的数据域。根据图,插入节点也就是:e->next = head->next; head->next = e;
增加链表节点用到了两个结构体指针和一个int数据。
void insert(LinkList *list, int n) {
LinkList *t = list, *in;
int i = 0;
while (i < n && t != NULL) {
t = t->next;
i++;
}
if (t != NULL) {
in = (LinkList*)malloc(sizeof(LinkList));
puts("输入要插入的值");
scanf("%d", &in->score);
in->next = t->next;//填充in节点的指针域,也就是说把in的指针域指向t的下一个节点
t->next = in;//填充t节点的指针域,把t的指针域重新指向in
}
else {
puts("节点不存在");
}
}
输出链表
输出链表很简单,边遍历边输出就行了。
while (h->next != NULL) {
h = h->next;
printf("%d ", h->score);
栈是一种操作受限的线性表只允许从一端插入和删除数据。栈有两种存储方式,即线性存储和链接存储(链表)。栈的一个最重要的特征就是栈的插入和删除只能在栈顶进行,所以每次删除的元素都是最后进栈的元素,故栈也被称为后进先出(LIFO)表。每个栈都有一个栈顶指针,它初始值为-1,且总是指向最后一个入栈的元素,栈有两种处理方式,即进栈(push)和出栈(pop),因为在进栈只需要移动一个变量存储空间,所以它的时间复杂度为O(1),但是对于出栈分两种情况,栈未满时,时间复杂度也为O(1),但是当栈满时,需要重新分配内存,并移动栈内所有数据,所以此时的时间复杂度为O(n)。以下举例栈结构的两种实现方式,线性存储和链接存储。
1、线性存储
线性存储的方式和线性表基本一致,只不过多了后进先出的限制而已,它是静态分配的,即使用前,它的内存就已经以数组的形式分配好了,所以在初始化时,需要指明栈的节点最大个数。
#include "stdafx.h"
#include
#include
//定义
template
class LineStack
{
public :
LineStack(int MaxListSize=10); //构造函数
~LineStack() //析构函数
{
delete[] elements;
}
bool IsEmpty() const //判断是否为空
{
return top==-1;
}
bool IsFull() const //判断是否满
{
return top==MaxSize-1;
}
T pop(); //出栈
int push(const T& data); //进栈
T getPop(); //获取栈顶元素值
void clear(); //清空栈
void print() ;
private:
int top; //栈顶指针
int MaxSize; //数组最大长度
T *elements;//一维动态数组
};
//实现...
template
LineStack
{
//基于公式的线性表的构造函数
MaxSize = MaxListSize;
elements = new T[MaxSize];
top = -1;
}
template
T LineStack
{
if(IsEmpty())
{
return NULL;
}
return elements[top--];
}
template
int LineStack
{
if(IsFull())
{
return -1;
}
else
{
elements[++top] = data;
}
return 0;
}
template
T LineStack
{
if(IsEmpty())
{
return NULL;
}
return elements[top];
}
template
void LineStack
{
top = -1;
return true;
}
template
void LineStack
{
for(int i=0;i<=top;i++)
{
std:: cout<
}
void LineStackSample()
{
int j;
int a = 10,b = 20,c = 30,d = 40;
LineStack
std::cout<<"IsFull = "<
S.push(a);
S.push(b);
S.push(c);
S.push(d);
j = S.pop(); return; int main(int argc, _TCHAR* argv[]) // 栈动态.cpp : 定义控制台应用程序的入口点。 // 单链表.cpp : 定义控制台应用程序的入口点。 #include "stdafx.h" template template private: if(NULL == currNode) return position; if(0 == size) size++; if(p->next == currNode) p = p->next; tmp = currNode; if(currNode == head) if(NULL == currNode->next) freeNode(p); return 0; return; return ; return ; Node head = NULL; return; template Node return; template template link->DeleteNode(); return tmp; int main(int argc, _TCHAR* argv[]) list.push(a); j = list.pop(); j = list.pop(); j = list.pop(); j = list.pop(); j = list.pop(); std::cin.get(); 队列是一种先进先出的数据类型 队列通常可以分为两种类型: 一、顺序队列,采用顺序存储,当长度确定时使用。 顺序队列又有两种情况: ①使用数组存储队列的称为静态顺序队列。 ②使用动态分配的指针的称为动态顺序队列。 二、链式队列,采用链式存储,长度不确定时使用(由链表实现)。 由于链式队列跟链表差不多,所以在这里只针对循环(环形)队列来说明并实践。 入队(尾部入队) 在循环队列中,“队满”和“队空”的条件有可能是相同的,都是front ==rear,这种情况下,无法区别是“队满”还是“队空”。 针对这个问题,有3种可能的处理方法: 【这里采用了第3种处理方法】 (1)另设一个标志以区别是“队满”还是“队空”。(即入队/出队前检查是否“队满”/“队空”) (2)设一个计数器,此时甚至还可以省去一个指针。 (3)少用一个元素空间,即约定队头指针在队尾指针的下一位置时就作为“队满”的标志,即“队满”条件为:(pQueue->rear+1)%MAX_SIZE == pQueue->front。 随着入队、出队的进行,会使整个队列整体向后移动,就会出现上图中的现象:队尾指针已经移到了最后,即队尾出现溢出,无法再进行入队操作,然而实际上,此时队列中还有空闲空间,这种现象称为“假溢出”。 解决“假溢出”的三种办法: 采用了这种头尾相接的循环队列后,入队的队尾指针加1操作及出队的队头指针加1操作必须做相应的修改,以确保下标范围为0~Max_Size-1。对指针进行取模运算,就能使指针到达最大下标位置后回到0,符合“循环”队列的特点。 因此入队时队尾指针加1操作改为: pQueue->tail = (pQueue->tail+1) % MAX_SIZE; 入队时队尾指针加1操作改为: pQueue->head = (pQueue->head+1) % MAX_SIZE; 顺序队列 // #include //初始化顺序队列 //判断顺序队列是否为空 //判断顺序队列是否已满 //清空顺序队列 //入队 //出队 //获取队首元素 //打印队列 //测试 链队列 // #include typedef struct{ //链队头结点的存储结构 //初始化链队 //判断链队是否为空 //获取链队的长度 //入队 //出队 //获取链队的队头元素 //清空链队 //打印链队 //测试 1、数组 数组的特点: 元素数目固定;下标有界。 数组的操作: 按照下标进行读写。 2、数组的顺序表示和实现 因计算机的内存结构是一维的,因此用一维内存来表示多维数组,就必须按某种次序将数组元素排成一列序列,然后将这个线性序列存放在储存器中。 通常有两种顺序存储方式: (1)行优先顺序——将数组元素按行排列 在PASCAL、C语言中,数组就是按行优先顺序储存。 (2)列优先顺序——将数组按列向量排列。 在FORTRAN语言中,数组就是按列优先顺序储存的。 3、矩阵的压缩储存 利用2维数组描述矩阵 特殊矩阵: 为了节省储存空间,矩阵进行压缩存储:即为多个相同的非零的元素值分配一个储存空间;对0元素不分配空间。 (1)对称矩阵 只需对对称矩阵中n(n+1)/2个元素进行储存表示 (2)三角矩阵 (3)稀疏矩阵 if 一个m*n的矩阵含有t个非零元素,且t远远小于m*n,则称这个矩阵为稀疏矩阵 利用三元组表示法对稀疏矩阵进行压缩储存 用三项内容表示稀疏矩阵中的每个非零元素,进行形式为:(i,j,value) 其中,i 表示行序号,j 表示列序号,value 表示非零元素的值 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 //稀疏矩阵,非零元素比较少 const int max=1000; template typedef struct { int i, j; Object e; }Triple; template typedef struct { Triple int mu, nu, tu; }TSMatrix; 稀疏矩阵的转置 转置前矩阵为M,转置后为T,M的列为T的行,因此,要按M.data的列序转置 所得到的转置矩阵T的三元组表必定按行优先存放 转置算法: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 template void trans( TSMattrix { T,mu=M.nu; T.nu=M.mu; T.tu=M.tu; if(T.tu) { int q=0; for(int col=0; col< M.nu;col++) for(int p=0; p { if(M.data[p].j==col) { T.data[q].i=M.data[p].j; T.data[q].j=M.data[p].i; T.data[q].e=M.data[p].e; q++; } } } } 这个转置算法的时间复杂度为O(n*t),效率太低了 快速转置算法 一遍扫描先确定三元组的位置关系,二次扫描由位置关系装入三元组 为了预先确定矩阵M中的每一列的第一个非零元素在数组B中应有的位置,需要闲球的矩阵M中的每一列中非零元素的个数。 为此,需要设计两个一位数组num[0...n-1]和cpot[1..n-1] num[0...n-1]:统计M中每一列非零元素的个数 cpot[0...n-1]:由递推关系得出M中的每列第一个非零元素在B中的位置 cpot[col]=cpot[col-1]+num[col-1]; 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 template void fasttranstri( TSMatrix { int q; int *num=new int[M.nu]; int *copt=new int[M.nu]; T.mu=M.nu; T.nu=M.mu; T.tu=M.tu; if(M.tu) { for(int col=0; col num[col]=0; //统计每列非零元素个数 for(int k=0; col ++num[m.data[k].j]; //每列第一个非零元素在T中位置 copt[0]=0; for(int col=1; col copt[col]=copt[col-1]+num[col-1]; for(int p=0; p { int col=M.data[p].j; q=cpot[col]; T.data[q].i=M.data[p].j; T.data[q].j=M.data[p].i; T.data[q].e=M.data[p].e; ++copt[col]; } } delete[] num; delete[] cpot; } 时间复杂度为O(n+t) 4、广义表的定义 第 6 章 树和二叉树 0x01 树的概念 把它叫做“树”是因为它看起来像一棵倒挂的树,也就是说它是根朝上,而叶朝下的。它具有以下的特点: 2. 树的基本术语 结点的度:结点拥有的子树的数目。 层次:根结点的层次为1,其余结点的层次等于该结点的双亲结点的层次加1。 0x02 二叉树的介绍 2. 二叉树的性质 2.1 性质1:二叉树第i层上的结点数目最多为 2{i-1} (i≥1) 2.2 性质2:深度为k的二叉树至多有2{k}-1个结点(k≥1) 证明:在具有相同深度的二叉树中,当每一层都含有最大结点数时,其树中结点数最多。利用"性质1"可知,深度为k的二叉树的结点数至多为: 2.3 性质3:包含n个结点的二叉树的高度至少为log2 (n+1) 2.4 性质4:在任意一棵二叉树中,若终端结点的个数为n0,度为2的结点数为n2,则n0=n2+1 3. 满二叉树,完全二叉树和二叉查找树 3.2 完全二叉树 3.3 二叉查找树 在实际应用中,二叉查找树的使用比较多。 结点的度:一个结点的子树数目称为该结点的度。(例如结点1的结点的度为3,结点2的结点的度为3,结点3的结点的度为0)。 树的度:所有结点度当中,度最高的一个。(上图树的度是3)。 叶子结点:上图应该是:3、5、6、7、9、10 分之结点:除了叶子结点,其他的都称为分之结点,和叶子结点构成互补的关系。(1、2、4、8) 内部结点:分之结点除了根结点以外的。(2、4、8) 父结点:如5号结点就是2号结点的子结点。 子结点:2号结点是5号结点的父结点。 兄弟结点:5、6、7称为兄弟结点,出自同一个父亲2号结点。 这三个概念是一个相对的概念。 层次:0层、1层、2层、3层。 还有一个公式就能做题了:总结点=所有度结点的和+1(应该是父结点) 前序遍历:先从根部出发,然后由左向右,一颗一颗树来完成遍历。先访问根再访问叶子结点,这就是我画出来的前序遍历图,箭头的方向表示遍历的顺序。 我们找五颗二叉树来进行分析:这样分析就简单多了,我都觉得不用分析了,但是还是说说吧。 2、深度为K的二叉树至多有2的(k+1)次方 -1个结点(k>=0)。(深度为二叉树中层数最大的叶节点的层数),满二叉树的深度为2,则共有7个结点。 3、对任何一颗二叉树,如果其叶子结点数为n0,度为2的结点数为n2,则n0=n2+1;(一定不要忘了根结点的度也是2)。 二叉树核心代码 线索二叉树原理 但是当我们希望得到二叉树中某一个结点的前驱或者后继结点时,普通的二叉树是无法直接得到的,只能通过遍历一次二叉树得到。每当涉及到求解前驱或者后继就需要将二叉树遍历一次,非常不方便。 于是是否能够改变原有的结构,将结点的前驱和后继的信息存储进来。 二叉树结构 观察二叉树的结构,我们发现指针域并没有充分的利用,有很多“NULL”,也就是存在很多空指针。 对于一个有n个结点的二叉链表,每个节点都有指向左右孩子的两个指针域,一共有2n个指针域。而n个结点的二叉树又有n-1条分支线数(除了头结点,每一条分支都指向一个结点),也就是存在2n-(n-1)=n+1个空指针域。这些指针域只是白白的浪费空间。因此, 可以用空链域来存放结点的前驱和后继。线索二叉树就是利用n+1个空链域来存放结点的前驱和后继结点的信息。 线索二叉树 如图以中序二叉树为例,我们可以把这颗二叉树中所有空指针域的lchild,改为指向当前结点的前驱(灰色箭头),把空指针域中的rchild,改为指向结点的后继(绿色箭头)。我们把指向前驱和后继的指针叫做线索 ,加上线索的二叉树就称之为线索二叉树。 线索二叉树结点结构 因此,我们在每一个结点都增设两个标志域LTag和RTag,它们只存放0或1的布尔型变量,占用的空间很小。于是结点的结构如图所示。 结点结构 其中: LTag为0是指向该结点的左孩子,为1时指向该结点的前驱 RTag为0是指向该结点的右孩子,为1时指向该结点的后继 因此实际的二叉链表图为 实际的二叉链表图 线索二叉树的结构实现 typedef char TElemType; 对普通二叉树以某种次序遍历使其成为线索二叉树的过程就叫做线索化。因为前驱和后继结点只有在二叉树的遍历过程中才能得到,所以线索化的具体过程就是在二叉树的遍历中修改空指针。 线索化具体实现 我们设置一个pre指针,永远指向遍历当前结点的前一个结点。若遍历的当前结点左指针域为空,也就是无左孩子,则把左孩子的指针指向pre(相对当前结点的前驱结点)。 右孩子同样的,当pre的右孩子为空,则把pre右孩子的指针指向当前结点(相对pre结点为后继结点)。 最后把当前结点赋给pre,完成后续的递归遍历线索化。 中序遍历线索化的递归函数代码如下: void InThreading(BiThrTree B,BiThrTree *pre) { 增设头结点 如上图,在线索二叉链表上添加一个head结点,并令其lchild域的指针指向二叉树的根结点(A),其rchild域的指针指向中序遍历访问的最后一个结点(G)。同样地,二叉树中序序列的第一个结点中,lchild域指针指向头结点,中序序列的最后一个结点rchild域指针也指向头结点。 于是从头结点开始,我们既可以从第一个结点顺后继结点遍历,也可以从最后一个结点起顺前驱遍历。就和双链表一样。 双链表 增设头结点并线索化的代码实现 //非递归遍历线索二叉树 } 森林和二叉树的转换 1、树转换为二叉树 由于二叉树是有序的,为了避免混淆,对于无序树,我们约定树中的每个结点的孩子结点按从左到右的顺序进行编号。 将树转换成二叉树的步骤是: 树转换为二叉树的过程示意图 森林是由若干棵树组成,可以将森林中的每棵树的根结点看作是兄弟,由于每棵树都可以转换为二叉树,所以森林也可以转换为二叉树。 将森林转换为二叉树的步骤是: 森林转换为二叉树的转换过程示意图 二叉树转换为树是树转换为二叉树的逆过程,其步骤是: 二叉树转换为森林比较简单,其步骤如下: 根据树与二叉树的转换关系以及二叉树的遍历定义可以推知,树的先序遍历与其转换的相应的二叉树的先序遍历的结果序列相同;树的后序遍历与其转换的二叉树的中序遍历的结果序列相同;树的层序遍历与其转换的二叉树的后序遍历的结果序列相同。由森林与二叉树的转换关系以及森林与二叉树的遍历定义可知,森林的先序遍历和中序遍历与所转换得到的二叉树的先序遍历和中序遍历的结果序列相同。 哈夫曼树 判定树: 在很多问题的处理过程中,需要进行大量的条件判断,这些判断结构的设计直接影响着程序的执行效率。例如,编制一个程序,将百分制转换成五个等级输出。大家可能认为这个程序很简单,并且很快就可以用下列形式编写出来: 哈夫曼编码 哈夫曼编码(Huffman Coding),又称霍夫曼编码,是一种编码方式,哈夫曼编码是可变字长编码(VLC)的一种。Huffman于1952年提出一种编码方法,该方法完全依据字符出现概率来构造异字头的平均长度最短的码字,有时称之为最佳编码,一般就叫做Huffman编码(有时也称为霍夫曼编码)。 数据的逻辑结构:树状结构。 数据的存储结构设计: (1)采用静态的三叉链表存放。 算法思想: 申请存储哈夫曼编码串的头指针数组,申请一个字符型指针,用来存放临时的编码串; 从叶子节点开始向上倒退,若其为它双亲节点的左孩子则编码标0,否则标1;直到根节点为止,最后把临时存储编码复制到对应的指针数组所指向的内存中。 重复上述步骤,直到所有的叶子节点都被编码完。 (2)设计一个结构体Element保存哈夫曼树中各结点的信息: struct Element //哈夫曼树结点的结构体 { char ch; //字符 int weight; //结点权值 int parent; //双亲指针 int lchild; //左孩子指针 int rchild; //右孩子指针 }; 示意图: weight ch Lchild Rchild parent 编码表节点结构: Struct HCode { char data; //字符 char code[100]; //编码内容 }; 哈夫曼算法的抽象数据类型定义: ADT Huffman DataModel 静态三叉链表存储数据 Operation Select 输入:哈夫曼树的三叉链表H[] 功能:选择权值最小的两棵树 输出:权值最小的两棵树 Reverse 输入:字符串 功能:倒置字符串 输出:倒置后的字符串 HuffmanTree 输入:哈夫曼树的三叉链表H[],字符的权值w[],字符集的大小n 功能:构建哈夫曼树 输出:无 CreateCodeTable 输入:哈夫曼树的三叉链表H[],哈夫曼编码表的三叉链表HC[],字符集的大小n 功能:给每个字符建立左右孩子 输出:哈夫曼编码表 Decode 输入:二进制编码 功能:解码 输出:解码后的字符串 endADT 5. 编码实现与静态检测 算法:HuffmanTree 输入:需要编译的字符和该字符的权值 输出:哈夫曼树 1.用户输入需要编译的字符和该字符的权值(即其字母的频度); 2.构造哈夫曼算法。设计一个数组H保存哈夫曼树中各结点的信息; 3.数组H初始化,将所有结点的孩子域和双亲域的值初始化为-1; 4.数组H的前n个元素的权值给定值; 5.调用select函数选择权值最小的根结点进行合并,其下标分别为i1,i2; 6.将二叉树i1,i2合并为一棵新的二叉树; 7.共进行n-1次合并,直到剩下一棵二叉树,这棵二叉树就是哈夫曼树; (2)创建编码表的伪代码如下: 算法:创建编码表 输入:哈夫曼树,编码表结构,字符个数 输出:每个字符的内容、权值、左右节点、双亲结点 1.根据已经创建的哈夫曼树创建编码表; 2.从叶子结点开始判断; 2.1如果当前叶子结点的双亲不是根结点,并且是其双亲的左孩子,则编码为‘0’,否则为‘1’; 2.2然后往上对其双亲进行判断,重复操作,直到每个字符编码完毕; 3.将已经完成的编码调用reserve函数进行倒置; 4.按照“下标n,权值weight,左孩子LChuld,右孩子RChild,双亲parent,字符char; (3)解码的伪代码如下: 算法:Decode 输入:二进制编码 输出:对应的字符串 1.用户输入要解码的二进制字符串,建立一个字符数组存储输入的二进制字符; 2.创建一个指向待解码的字符串的第1个字符的指针; 3.读取每一个字符。设置一个根结点的指针,从根结点开始判断; 3.1若字符为‘0’,则指向哈夫曼树当前结点的左孩子; 3.2若字符为‘1’,则指向当前结点的右孩子; 3.3直到指针指向的当前结点的左孩子为-1时,输出符合的字母; 4.输出解码结果; 时间复杂度: 1)Select函数,时间复杂度O(n) 2)Reverse函数,时间复杂度O(n) 3)HuffmanTree函数,构造哈夫曼树,时间复杂度O(n!) 4)CreateCodeTable函数,构造和输出哈夫曼编码表,时间复杂度O(n) 5)Decode函数,解码,时间复杂度O(n) 6. 上机调试
std::cout<
std::cout<
std::cout<
std::cout<
}
{
LineStackSample();
//暂停操作
char str;
std::cin>>str;
//程序结束
return 0;
}
2、链接存储
实际上只要对单链表类做适当的修改,限制其插入、删除、修改和访问结点的行为,使其符合栈先进后出的规则即可,另外需要单独提供栈访问的接口函数,例如进栈、出栈、获取栈大小等,链表知识可以参考https://blog.csdn.net/Chiang2018/article/details/82730174。
//
//
#include
#include
class Node
{
public:
T data;
Node(T& item);
Node
};
class LinkList
{
public:
LinkList();
~LinkList();
int getSize(void);
bool IsEmpty(void);
int gotoNext(void);
int getPostion(void);
int InsertNode(T& data);
int DeleteNode(void);
void getCurrNodeData(T& data);
void setCurrNodeData(T& data);
void clear();
void print();
Node
Node
int size;
int position;
void freeNode(Node
};
template
Node
{
data = item;
next = NULL;
}
template
LinkList
{
head = NULL;
currNode = NULL;
size = 0;
position = -1;
}
template
LinkList
{
clear();
}
template
int LinkList
{
return size;
}
template
bool LinkList
{
return size==0?true:false;
}
template
int LinkList
{
if(NULL == head)
{
return -1;
}
{
return -1;
}
else
{
currNode = currNode->next;
position++
}
}
template
int LinkList
{
return position;
}
template
int LinkList
{
Node
{
head = p;
head->next = NULL;
currNode = p;
}
else
{
p->next = currNode->next;
currNode->next = p;
currNode = p;
}
position++;
return size;
}
template
int LinkList
{
if(0 == size)
{
return -1;
}
Node
Node
for(int i = 0;i < size;i++)
{
if(NULL == p)
{
return -1;
}
{
p->next = currNode->next;
break;
}
}
{
head = currNode->next;
}
{
position--;
currNode = p;
}
else
{
currNode = currNode->next;
}
size--;
if(0 == size)
{
position = -1;
}
}
template
void LinkList
{
if(!p)
{
delete p;
}
}
template
void LinkList
{
if(currNode)
{
data = currNode->data;
}
}
template
void LinkList
{
if(currNode)
{
currNode->data = data;
}
}
template
void LinkList
{
if(0 == size)
{
return;
}
Node
while(p)
{
freeNode(p);
p = tmp;
if(tmp)
{
tmp = tmp->next;
}
}
currNode = NULL;
size = 0;
position = -1;
}
void LinkList
{
if(0 == size)
{
return;
}
Node
while(p)
{
std::cout<
if(tmp)
{
tmp = tmp->next;
}
}
}
template
class LinkedStack
{
private:
LinkList
public:
LinkedStack();
void push(T& data);
T pop();
void ClearStack();
int getSize();
bool isEmpty();
};
template
LinkedStack
{
link = new LinkList
}
template
bool LinkedStack
{
return link->IsEmpty();
}
template
int LinkedStack
{
return link->size;
}
void LinkedStack
{
link->clear();
}
template
void LinkedStack
{
link->InsertNode(data);
}
T LinkedStack
{
T tmp;
if(isEmpty())
{
return NULL;
}
link->getCurrNodeData(tmp);
}
{
int j = 0;
int a = 10,b=20,c=30,d=40,e=50;
LinkedStack
list.push(b);
list.push(c);
list.push(d);
list.push(e);
std::cout<
std::cout<
std::cout<
std::cout<
std::cout<
return 0;
}
循环队列的两个参数:
①front,front指向队列的第一个元素。(front==head)
②rear,rear指向队列的最后一个有效元素的下一元素。(rear==tail)
队列的两个基本操作:出队和入队。
①将值存入rear所代表的位置。
②rear = (rear+1)%数组的长度。
出队(头部出队)
front = (front+1)%数组的长度。
队列是否为空
front和rear的值相等,则该队列就一定为空。
队列是否已满
// main.c
// SequenceQueue
//
// Created by Eason on 2020/8/2.
// Copyright © 2020 Eason. All rights reserved.
//
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
#define MAXSIZE 21 //采用循环队列的方式,充分利用空间,但是会有一个单元的浪费,实际容量为最大容量-1,因为要区分空与满
typedef int ElemType;
typedef int Status;
typedef struct{ //顺序队列的存储结构
ElemType data[MAXSIZE]; //队列元素的数据
int front, rear; //头指针与尾指针
}SqQueue;
Status initQueue(SqQueue *Q){
Q->front=0; //初始时头指针与为指针均指向0
Q->rear=0;
return OK;
}
Status isEmpty(SqQueue Q){
if(Q.front==Q.rear){ //当头指针与尾指针指向相同的单元时表示队列为空
return TRUE;
}else{
return FALSE;
}
}
Status isFull(SqQueue Q){
if((Q.rear+1)%MAXSIZE==Q.front){ //循环队列头指针可能在前也可能在后,头指针在前尾指针在最后差1时为满,头指针在后尾指针与头指针差1时为满,则用差值取余的方式可判断当前是否已满
return TRUE;
}else{
return FALSE;
}
}
//获取顺序队列的长度
Status getLength(SqQueue Q){
return (Q.rear-Q.front+MAXSIZE)%MAXSIZE; //根据循环队列的特点,即长度为差值加最大容量与最大容量的取模运算
}
Status clearQueue(SqQueue *Q){
Q->front = Q->rear; //根据队列为空的判断条件,即将头指针指向尾指针时两指针指向同一单元时队列为空。不能颠倒,因为尾指针指向的是空单元
return OK;
}
Status enter(SqQueue *Q, ElemType *e){
if(isFull(*Q)){ //判断队列是否还可以再入队元素
printf("队列已满,无法入队n");
return ERROR;
}
Q->data[Q->rear]=e; //将当前尾指针指向的空白单元入值
Q->rear = (Q->rear+1)%MAXSIZE; //将尾指针向后走一格,因为有可能是从队尾到队头,所以使用取模的方式判断尾指针接下来的位置
return OK;
}
Status leave(SqQueue *Q, ElemType *e){
if(isEmpty(*Q)){ //判断队列是否有元素可以出队
printf("队列为空,不可出队n");
return ERROR;
}
*e = Q->data[Q->front]; //将当前队首元素出队给e供返回与查看
Q->front = (Q->front+1)%MAXSIZE; //将头指针向后走一格,因为有可能是从队尾到队头,所以使用取模的方式判断头指针接下来的位置
return OK;
}
Status getHead(SqQueue Q, ElemType *e){
if(isEmpty(Q)){ //判断队列是否有元素可供查看
printf("队列为空,无队首元素n");
return ERROR;
}
*e = Q.data[Q.front]; //将当前队头元素返回给e供返回并查看
return OK;
}
Status printQueue(SqQueue Q){
if(isEmpty(Q)){ //判断队列当前是否有元素可供打印
printf("队列为空,无队元素可供打印n");
return ERROR;
}
int i=0; //当前元素数量计数器
while(!isEmpty(Q)){
i++; //不为空说明当前有元素可供打印,则计数器+1
printf("从队首至队尾的第%d个元素为:%dn", i, Q.data[Q.front]); //打印当前队首元素
Q.front = (Q.front+1)%MAXSIZE; //将头指针向后走一格,因为有可能是从队尾到队头,所以使用取模的方式判断头指针接下来的位置
}
return OK;
}
int main(int argc, const char * argv[]) {
SqQueue Q;
initQueue(&Q);
printf("初始化队列Q后队列的长度为:%d,是否为空?(是1否0):%dn", getLength(Q), isEmpty(Q));
printf("将1-18顺序的入队后队列变为:n");
for(int i=1;i<=18;i++){
enter(&Q, i);
}
printQueue(Q);
printf("此时队列的长度为:%dn", getLength(Q));
printf("队列的最大长度为20,检验再依次入队3个元素后得:n");
enter(&Q, 19);
enter(&Q, 20);
enter(&Q, 21);
printQueue(Q);
printf("-----验证循环队列-----n");
int e;
leave(&Q, &e);
printf("出队列:%dn", e);
enter(&Q, 21);
printf("入队列:21n");
printf("此时的队列内容为:n");
printQueue(Q);
printf("此时队列的长度为:%d,是否已满?(1是0否):%dn", getLength(Q), isFull(Q));
printf("清空队列后队列为:n");
clearQueue(&Q);
printQueue(Q);
return 0;
}
// main.c
// LinkedQueue
//
// Created by Eason on 2020/8/2.
// Copyright © 2020 Eason. All rights reserved.
//
#include
#define OK 1
#define ERROR 0
#define TRUE 1
#define FALSE 0
typedef int ElemType;
typedef int Status; //注意:头指针始终指向头结点(不存放数据)头结点的下一结点才是链队的队头
typedef struct QueueNode{ //链队结点的存储结构
ElemType data; //数据域
struct QueueNode *next; //指针域
}QueueNode, *QueueNodePtr;
QueueNodePtr front, rear; //头指针与尾指针
}LinkQueue;
Status initQueue(LinkQueue *Q){
Q->front = (QueueNodePtr)malloc(sizeof(QueueNode)); //初始化为首位指针分配内存
Q->rear = (QueueNodePtr)malloc(sizeof(QueueNode));
if(!Q->front || !Q->rear){ //若不存在则说明内存分配失败无法初始化
printf("初始化内存分配失败");
return ERROR;
}
Q->front = Q->rear; //空队的条件 队头指针=队尾指针 均指向头结点
Q->front->next = NULL; //头结点的下一结点,即首个结点为空
return OK; //这里要注意,初始的时候首位指针均指向头结点,是不存放数据的,链队此时为空
}
Status isEmpty(LinkQueue Q){
if(Q.front==Q.rear){ //链队为空的条件为头指针=尾指针,即都指向头结点
return TRUE;
}else{
return FALSE;
}
}
int getLength(LinkQueue Q){
QueueNodePtr p; //定义临时结点指针
p = Q.front->next; //将临时结点置为链队的首个结点
int i=0; //链队长度计数器
while(p){ //如果此时链队结点存在的话就继续向下进行
i++; //计数器+1
p = p->next; //链队指针继续向下移动
}
return i; //返回链队计数器i即长度
}
Status enter(LinkQueue *Q, ElemType *e){
QueueNodePtr p = (QueueNodePtr)malloc(sizeof(QueueNode)); //为新入队的结点分配内存空间
if(!p){ //验证是否为新结点内存分配成功
printf("新结点内存分配失败,无法入队");
return ERROR;
}
p->data = e; //将新结点的数据域置为指定值
Q->rear->next = p; //还未入队的此时队尾的下一结点指向p
Q->rear = p; //这时队尾指针指向p,即p成为了新的队尾结点
p->next = NULL; //队尾结点的指针域为空
return OK;
}
Status leave(LinkQueue *Q, ElemType *e){
if(isEmpty(*Q)){ //判断链队是否为空,若为空则无链队元素可供出队
printf("链队为空,无队元素可出列n");
return ERROR;
}
QueueNodePtr p; //定义临时结点指针p
p = Q->front->next; //将p置为链队的首个结点
*e = p->data; //将首个结点p的数据域赋给e以供返回与查看
Q->front->next = p->next; //将老首个结点的下一结点设置为首个结点
if(Q->rear == p){ //若此时p也为尾结点,说明队列为空了
Q->rear = Q->front; //如果队列为空则将队尾指针重新指向头结点表示链队为空了
}
free(p); //将出队的p结点释放掉
return OK;
}
Status getHead(LinkQueue Q, ElemType *e){
if(isEmpty(Q)){ //判断链队是否为空,若为空则无链队的队头元素可供查看
printf("链队为空,无队头元素n");
return ERROR;
}
*e = Q.front->next->data; //若链队不为空,则将链队的首个结点的数据域赋给e供返回与查看
return OK; //仅查看队头元素,指针不发生移动
}
Status clearQueue(LinkQueue *Q){
QueueNodePtr p, q; //定义临时结点pq
p = Q->front->next; //将p结点置为链队的首个结点
Q->rear = Q->front; //将队尾指针指向头结点与头指针相同,这样则表示此时链队为空
Q->front->next = NULL; //将头结点的指针域置空,即无首个结点
while(p){ //若p存在则继续执行循环体
q = p; //将p赋给q
p = p->next; //p继续向下进行
free(q); //释放q,一直循环直到p结点不再存在,即此时链队的结点都被释放掉了
}
return OK;
}
Status printQueue(LinkQueue Q){
if(isEmpty(Q)){ //判断链队是否为空,若为空则无链队元素可供打印
printf("当前链队无元素可供打印n");
return ERROR;
}
QueueNodePtr p; //定义临时结点p
p = Q.front->next; //将p赋为链队的首个结点
int i=0; //此时链队元素位置计数器
while(p){ //如果此时p存在的话则继续执行循环体
i++; //p存在则计数器+1
printf("从队首至队尾的第%d个元素为:%dn", i, p->data); //打印当前p结点的数据域
p = p->next; //p指针继续向下移动,直到p为空,即链队的所有结点都打印完毕
}
return OK;
}
int main(int argc, const char * argv[]) {
LinkQueue Q;
initQueue(&Q);
printf("初始化队列Q后队列的长度为:%dn此时的链队是否为空?(1是0否):%dn", getLength(Q), isEmpty(Q));
printf("将1-6按顺序入队后可得链队:n");
for(int i=1;i<=6;i++){
enter(&Q, i);
}
printQueue(Q);
printf("此时队列的长度为:%dn", getLength(Q));
int e;
getHead(Q, &e);
printf("此时队列的队头为:%dn", e);
printf("队列出队两个元素:n");
leave(&Q, &e);
printf("出队:%dn", e);
leave(&Q, &e);
printf("出队:%dn", e);
printf("现在的链队为:n");
printQueue(Q);
printf("现在的链队长度为:%dn", getLength(Q));
getHead(Q, &e);
printf("现在链队的队头为:%dn", e);
printf("清空链队后得链队为:n");
clearQueue(&Q);
printQueue(Q);
return 0;
}
1. 树的定义
树是一种数据结构,它是由n(n>=1)个有限节点组成一个具有层次关系的集合。
(01) 每个节点有零个或多个子节点;
(02) 没有父节点的节点称为根节点;
(03) 每一个非根节点有且只有一个父节点;
(04) 除了根节点外,每个子节点可以分为多个不相交的子树。
若一个结点有子树,那么该结点称为子树根的"双亲",子树的根是该结点的"孩子"。有相同双亲的结点互为"兄弟"。一个结点的所有子树上的任何结点都是该结点的后裔。从根结点到某个结点的路径上的所有结点都是该结点的祖先。
叶子:度为零的结点。
分支结点:度不为零的结点。
树的度:树中结点的最大的度。
树的高度:树中结点的最大层次。
无序树:如果树中结点的各子树之间的次序是不重要的,可以交换位置。
有序树:如果树中结点的各子树之间的次序是重要的, 不可以交换位置。
森林:0个或多个不相交的树组成。对森林加上一个根,森林即成为树;删去根,树即成为森林。
1. 二叉树的定义
二叉树是每个节点最多有两个子树的树结构。它有五种基本形态:二叉树可以是空集;根可以有空的左子树或右子树;或者左、右子树皆为空。
二叉树有以下几个性质:TODO(上标和下标)
性质1:二叉树第i层上的结点数目最多为 2{i-1} (i≥1)。
性质2:深度为k的二叉树至多有2{k}-1个结点(k≥1)。
性质3:包含n个结点的二叉树的高度至少为log2 (n+1)。
性质4:在任意一棵二叉树中,若终端结点的个数为n0,度为2的结点数为n2,则n0=n2+1。
证明:下面用"数学归纳法"进行证明。
(01) 当i=1时,第i层的节点数目为2{i-1}=2{0}=1。因为第1层上只有一个根结点,所以命题成立。
(02) 假设当i>1,第i层的节点数目为2{i-1}。这个是根据(01)推断出来的!
下面根据这个假设,推断出"第(i+1)层的节点数目为2{i}“即可。
由于二叉树的每个结点至多有两个孩子,故"第(i+1)层上的结点数目” 最多是 “第i层的结点数目的2倍”。即,第(i+1)层上的结点数目最大值=2×2{i-1}=2{i}。
故假设成立,原命题得证!
20+21+…+2k-1=2k-1
故原命题得证!
证明:根据"性质2"可知,高度为h的二叉树最多有2{h}–1个结点。反之,对于包含n个节点的二叉树的高度至少为log2(n+1)。
证明:因为二叉树中所有结点的度数均不大于2,所以结点总数(记为n)=“0度结点数(n0)” + “1度结点数(n1)” + “2度结点数(n2)”。由此,得到等式一。
(等式一) n=n0+n1+n2
另一方面,0度结点没有孩子,1度结点有一个孩子,2度结点有两个孩子,故二叉树中孩子结点总数是:n1+2n2。此外,只有根不是任何结点的孩子。故二叉树中的结点总数又可表示为等式二。
(等式二) n=n1+2n2+1
由(等式一)和(等式二)计算得到:n0=n2+1。原命题得证!
3.1 满二叉树
定义:高度为h,并且由2{h} –1个结点的二叉树,被称为满二叉树。
定义:一棵二叉树中,只有最下面两层结点的度可以小于2,并且最下一层的叶结点集中在靠左的若干位置上。这样的二叉树称为完全二叉树。
特点:叶子结点只能出现在最下层和次下层,且最下层的叶子结点集中在树的左部。显然,一棵满二叉树必定是一棵完全二叉树,而完全二叉树未必是满二叉树。
定义:二叉查找树(Binary Search Tree),又被称为二叉搜索树。设x为二叉查找树中的一个结点,x节点包含关键字key,节点x的key值记为key[x]。如果y是x的左子树中的一个结点,则key[y] <= key[x];如果y是x的右子树的一个结点,则key[y] >= key[x]。
另一种定义:根节点的值大于等于其左子树中任意一个节点的值,小于等于其右节点中任意一节点的值,这一规则适用于二叉查找树中的每一个节点。
在二叉查找树中:
(01) 若任意节点的左子树不空,则左子树上所有结点的值均小于它的根结点的值;
(02) 任意节点的右子树不空,则右子树上所有结点的值均大于它的根结点的值;
(03) 任意节点的左、右子树也分别为二叉查找树。
(04) 没有键值相等的节点(no duplicate nodes)。
树的遍历:
后序遍历:顾名思义,就是从叶子结点出发,先遍历叶子结点再到根结点,最后到父结点。
层次遍历:按0层、1层、2层、3层,从左到右来遍历
我们接着就可以来理解二叉树的重要的特性:
1、二叉树中,第i层上最多有2的i次方个结点(i>=0)。这个很基本,这也是二叉树和树的区别。
#include
遍历二叉树的其实就是以一定规则将二叉树中的结点排列成一个线性序列,得到二叉树中结点的先序序列、中序序列或后序序列。这些线性序列中的每一个元素都有且仅有一个前驱结点和后继结点。
如果只是在原二叉树的基础上利用空结点,那么就存在着这么一个问题:我们如何知道某一结点的lchild是指向他的左孩子还是指向前驱结点?rchild是指向右孩子还是后继结点?显然我们要对他的指向增设标志来加以区分。
二叉树的线索存储结构定义如下:
typedef enum { Link, Thread } PointerTag; //Link==0,表示指向左右孩子指针
//Thread==1,表示指向前驱或后继的线索
//二叉树线索结点存储结构
typedef struct BiThrNode {
TElemType data; //结点数据
struct BiThrNode *lchild, *rchild; //左右孩子指针
PointerTag LTag;
PointerTag RTag; //左右标志
}BiThrNode, *BiThrTree;
二叉树线索化
线索化
以中序二叉树的线索化为例,线索化的具体实现就是将中序二叉树的遍历进行修改,把原本打印函数的代码改为指针修改的代码就可以了。
if(!B) return;
InThreading(B->lchild,pre);
//--------------------中间为修改空指针代码---------------------
if(!B->lchild){ //没有左孩子
B->LTag = Thread; //修改标志域为前驱线索
B->lchild = *pre; //左孩子指向前驱结点
}
if(!(*pre)->rchild){ //没有右孩子
(*pre)->RTag = Thread; //修改标志域为后继线索
(*pre)->rchild = B; //前驱右孩子指向当前结点
}
*pre = B; //保持pre指向p的前驱
//---------------------------------------------------------
InThreading(B->rchild,pre);
}
增设头结点
线索化后的二叉树,就如同操作一个双向链表。于是我们想到为二叉树增设一个头结点,这样就和双向链表一样,即能够从第一个结点正向开始遍历,也可以从最后一个结点逆向遍历。
//为线索二叉树添加头结点,使之可以双向操作
Status InOrderThreading(BiThrTree *Thrt,BiThrTree T){
if(!(*Thrt = (BiThrTree)malloc(sizeof(BiThrNode)))) exit(OVERFLOW); //开辟结点
(*Thrt)->LTag = Link;
(*Thrt)->RTag = Thread; //设置标志域
(*Thrt)->rchild = (*Thrt); //右结点指向本身
if(!T) {
(*Thrt)->lchild = (*Thrt);
return OK; //若根结点不存在,则该二叉树为空,让该头结点指向自身.
}
BiThrTree pre; //设置前驱结点
//令头结点的左指针指向根结点
pre = (*Thrt);
(*Thrt)->lchild = T;
//开始递归输入线索化
InThreading(T,&pre);
//此时结束了最后一个结点的线索化了,下面的代码把头结点的后继指向了最后一个结点.
//并把最后一个结点的后继也指向头结点,此时树成为了一个类似双向链表的循环.
pre->rchild = *Thrt;
pre->RTag = Thread;
(*Thrt)->rchild = pre;
return OK;
}
遍历线索二叉树
线索二叉树的遍历就可以通过之前建立好的线索,沿着后继线索依依访问下去就行。
Status InOrderTraverse(BiThrTree T) {
BiThrNode *p = T->lchild;
while(p!=T){
while(p->LTag==Link) p = p->lchild; //走向左子树的尽头
printf("%c",p->data );
while(p->RTag==Thread && p->rchild!=T) { //访问该结点的后续结点
p = p->rchild;
printf("%c",p->data );
}
p = p->rchild;
}
return OK;
(1)加线。就是在所有兄弟结点之间加一条连线;
(2)抹线。就是对树中的每个结点,只保留他与第一个孩子结点之间的连线,删除它与其它孩子结点之间的连线;
(3)旋转。就是以树的根结点为轴心,将整棵树顺时针旋转一定角度,使之结构层次分明。
2、森林转换为二叉树
(1)先把每棵树转换为二叉树;
(2)第一棵二叉树不动,从第二棵二叉树开始,依次把后一棵二叉树的根结点作为前一棵二叉树的根结点的右孩子结点,用线连接起来。当所有的二叉树连接起来后得到的二叉树就是由森林转换得到的二叉树。
3、二叉树转换为树
(1)若某结点的左孩子结点存在,将左孩子结点的右孩子结点、右孩子结点的右孩子结点……都作为该结点的孩子结点,将该结点与这些右孩子结点用线连接起来;
(2)删除原二叉树中所有结点与其右孩子结点的连线;
(3)整理(1)和(2)两步得到的树,使之结构层次分明。
二叉树转换为树的过程示意图
4、二叉树转换为森林
(1)先把每个结点与右孩子结点的连线删除,得到分离的二叉树;
(2)把分离后的每棵二叉树转换为树;
(3)整理第(2)步得到的树,使之规范,这样得到森林。
if(score<60)
cout<<"Bad"<
cout<<"Pass"<
cout<<"General"<
cout<<"Good"<
cout<<"Very good!"<
若考虑上述程序所耗费的时间,就会发现该程序的缺陷。在实际中,学生成绩在五个等级上的分布是不均匀的。当学生百分制成绩的录入量很大时,上述判定过程需要反复调用,此时程序的执行效率将成为一个严重问题。
但在实际应用中,往往各个分数段的分布并不是均匀的。下面就是在一次考试中某门课程的各分数段的分布情况:
我们利用哈夫曼树寻找一棵最佳判定树,即总的比较次数最少的判定树。
我们称判定过程最优的二叉树为哈夫曼树,又称最优二叉树
(1)创建哈夫曼树的伪代码如下:#include


