数据结构
✍️ Author: Michael Zhao 📅 Start: 2023/11/24
📓 注:所有课后习题都要过一遍,复习基础采用刷选择题~
一.绪论
线性表(数据特性相同):顺序表(顺序存储结构,一种随机存取结构)、链表等(链式存储结构,一种非随机存取结构)。
数据元素是数据的基本单位;数据项是组成数据元素的不可分割的最小单位;数据对象:性质相同的数据元素的集合。
逻辑结构:集合结构、线性结构、树结构、图结构或网状结构。
存储结构:顺序存储结构,链式存储结构。
O(f(n)) 中的O表示数量级。T(n):时间复杂度S(n):空间复杂度。一般算法都有以时间复杂度为评判标准。
顺序存储方式:线性结构,树形结构,图状结构都可以
课后习题
算法的复杂度取决于问题的规模和待处理数据的状态
数据结构是带有结构的各数据元素的集合
相同数据元素:数据项个数相同,类型一致
二.线性表
顺序表
typedef struct{
Type *elem;//记住elem是指针
int length;
}SqList;
//初始化
int InitList(SqList){
L.elem=new Type[100];
if(!L.elem) exit(0);
L.length=0;
return 0;
}
//取值
int GetElem(SqList L,int i,Type &e){
if(i<1||i>L.length) return ERROR;
e=L.elem[i-1];
return OK;
}
//查找
int LocateElem(SqList L,Type &e){
for(i=0;i<L.length;i++)
if(L.elem[i]==e) return i+1;
return 0;
}
//插入 O(n)
int ListInsert(SqList &L,int i,Type e){
if((i<1)||(i>L.length+1)) return ERROR;//插入的最大合法位置为L.length+1
if(L.length==MAXSIZE) return ERROR;
for(j=L.length-1;j>=i-1;j--)
L.elem[j+1]=L.elem[j];
L.elem[i-1]=e;//第i个位置的角标是i-1
++L.length;
return OK;
}
//删除 O(n)
int ListDelete(SqList &L,int i){
if((i<1)||(i>L.length+1)) return ERROR;
for(j=L.length-1;j>=i-1;j--)
L.elem[j-1]=L.elem[j];
--L.length;
return OK;
}
#include <stdlib.h> // 动态分配
malloc(int);//开辟int大小的空间
free(p);//释放*p的指针
平均查找长度:ASL。
链表
单链表
单链表:每个节点只包含一个指针域。
数组作参的时候传的是地址。
typedef struct{
Type data;
struct LNode *next;
}LNode,*LinkList;
//初始化
int InitList(LinkList &L){
L=new LNode;
L->next=nullptr;
return OK;
}
//取值
int GetElem(LinkList L,int i,Type &e){
p=L->next;
j=1;
while(p&&j<i){
p=p->next;
++j;
}
if(!p||j>i) return ERROR;
e=p->data;
return OK;
}
//查找
LNode *LocateElem(LinkList L,Type e){
p=L->next;
while(p&&p->data!=e){
p=p->next;
}
return p;
}
//插入
int ListInsert(LinkList &L,int i,Type e){
p=L;j=0;
while(p&&(j<i-1)){
p=p->next;
++j;
}
if(!p||j>i-1) return ERROR;
s=new LNode;
s->data=e;
s->next=p->next;
p->next=s;
return OK;
}
//删除
int ListDelete(LinkList &L,int i){
p=L;j=0;
while((p->next)&&(j<i-1)){
p=p->next;
++j;
}
if(!(p->next)||(j>i-1)) return ERROR;
*q=p->next;
p->next=q->next;
delete q;
return OK;
}
区分好头指针,头节点,首元节点。
前插法
void CreatList_H(LinkList &L,int n){
L=new LNode;
L->next=nullptr;
for(int i=0;i<n;i++){
p=new LNode;
cin>>p->data;
p->next=L->next;
L->next=p;
}
}
后插法
void CreatList_R(LinkList &L,int n){
L=new LNode;
L->next=nullptr;
r=L;
for(int i=0;i<n;i++){
p=new LNode;
cin>>p->data;
p->next=nullptr;
r->next=p;
r=p;//r向后面移动一位,及到尾部,所以为尾插法。
}
}
循环链表
p!=L;p->next!=L;//判断条件
p=B->next->next;
B->next=A->next;
A->next=p;
双向链表
typedef struct DuLNode{
Type data;
struct DuLNode *prior;
struct DuLNode *next;
}DuLNode,*DuLinkList;
//插入,千万注意顺序
int ListInsert_DuL(DuLinkList &L,int i,Type e){
if(!(p=GetElem_DuL(L,i))) return ERROR;
s=new DuLNode;
s->data=e;
s->prior=p->prior;
p->prior->next=s;
s->next=p;
p->prior=s;
return OK;
}
//删除
int ListDelete_DuL(DuLinkList &L,int i){
if(!(p=GetElem_DuL(L,i))) return ERROR;
p->prior->next=p->next;
p->next->prior=p->prior;//都是承接最后一个p->
delete p;
}
链表的存储密度较低
线性表的合并
void MergeList(List &LA,List LB){
int m=LA.length;
int n=LB.length;
for(int i=1;i<=n;i++){
GetElem(LB,i,e);
if(!Locate(LA,e)){
ListInsert(LA,++m,e);
}
}
}
时间复杂度为O(mn),原因在于GetElem(LB,i,e);和ListInsert(LA,++m,e);
顺序有序表的合并
void MergeList_Sq(SqList LA,SqList LB,SqList &LC){
LC.length=LA.length+LB.length;
LC.elem=new Type[LC.length];
Type *pc=LC.elem;
Type *pa=LA.elem;
Type *pb=LB.elem;
Type *pa_last=LA.elem+LA.length-1;
Type *pb_last=LB.elem+LB.length-1;
while((pa<=pa_last)&&(pb<=pb_last)){
if(*pa<=*pb) *pc++=*pa++;
else *pc++=*pb++;
while(pa<=pa_last) *pc++=*pa++;
while(pb<=pb_last) *pc++=*pb++;
}
}
时间复杂度为O(m+n)
链式有序表的合并
void MergeList_L(LinkList &LA,LinkList &LB,LinkList &LC){
LNode *pa=LA->next;
LNode *pb=LB->next;
LNode *LC=*pc=LA;
while(pa&&pb){
if(pa->data<=pb->data){
pc->next=pa;
pc=pa;
pa=pa->next;
}else{
pc->next=pb;
pc=pb;
pb=pb->next;
}
}
pc->next=pa?pa:pb;
delete LB;
}
空间复杂度O(1)
多项式相加
void AddPolyn(Polynomial *pa,Polynomial *pb){
Polynomial *p1=pa->next;
Polynomial *p2=pb->next;
Polynomial *p3=pa;
while(p1&&p2){
if(p1->expn=p2->expn){
sum=p1->coef+p2->coef;
if(sum!=0){
p1->coef=sum;
p3->next=p1;//直接在pa的头上改的原因就是可以节省内存
p3=p1;
p1=p1->next;
r=p2;p2=p2->next;delete r;
}else{
r=p1;p1=p1->next;delete r;
r=p1;p1=p1->next;delete r;
}
}else if(p1->expn<p2->expn){
p3->next=p1;
p3=p1;
p1=p1->next;
}else{
p3->next=p2;
p3=p2;
p2=p2->next;
}
}
p3->next=p1?p1:p2;
delete pb;//在最后一步全部赋值到p3上,且都以pa为基准,所以pb需要删去。
}
顺序存储结构:时间复杂度O(1);链式存储结构:时间复杂度O(n)
课后习题
两个各含有n个元素的有序表归并成一个有序表,其最少得比较次数是:2n
线性表并不是至少有一个元素,可以为空
创建一个包含n个结点的有序单链表的时间复杂度是:O(n^2),(n+1)*n/2是二次方类型的
由于顺序存储要求连续的存储区域,因此在存储管理上不够灵活
精选习题
void CreateHeadList(LinkList &L, ElemType a[], int n )
{
//根据长度为n的数组a用头插法来创建单链表L
/********** Begin **********/
L=new LNnode;//一开始还没有分配空间呢
L->next=NULL;
LinkList p=L;//换取一个指针,免得改变之前的
for(int i=0;i<n;i++){
LinkList s=new LNnode;
s->data=a[i];
s->next=p->next;
p->next=s;
}
/********** End **********/
}
void BubbleSort(LinkList &head,int (*compare)(ElemType,ElemType))
{
//用冒泡法将带头结点的单链表排成一个有序的单链表
/********** Begin **********/
LinkList p1,p2;
p1=head->next;
while(p1){
p2=p1->next;
while(p2){
if(compare(p1->data,p2->data)>0){
swap(p1->data,p2->data);
}
p2=p2->next;
}
p1=p1->next;
}
/********** End **********/
}
int i, temp;
for (i = 0; i < A.length / 2; i++) // 交换前半部分和后半部分的元素
{
temp = A.elem[i];
A.elem[i] = A.elem[A.length - i - 1];
A.elem[A.length - i - 1] = temp;
}
int MergeList(SqList La, SqList Lb, SqList &Lc)
{
// 已知顺序线性表La和Lb的元素按值非递减排列。
// 归并La和Lb得到新的顺序线性表Lc,Lc的元素也按值非递减排列。
/********** Begin **********/
Lc.length=La.length+Lb.length;
Lc.elem=new ElemType[Lc.length];
ElemType *pc=Lc.elem;
ElemType *pa=La.elem;
ElemType *pb=Lb.elem;
ElemType *pal=La.elem+La.length-1;
ElemType *pbl=Lb.elem+Lb.length-1;
while((pa<=pal)&&(pb<=pbl)){
if(*pa<*pb){
*pc++=*pa++;
}else{
*pc++=*pb++;
}
}
while(pa<=pal) *pc++=*pa++;
while(pb<=pbl) *pc++=*pb++;
/********** End **********/
}
int MergeList(SqList La, SqList Lb, SqList &Lc)
{
// 已知顺序线性表La和Lb的元素按值非递减排列。
// 归并La和Lb得到新的顺序线性表Lc,Lc的元素也按值非递减排列。
/********** Begin **********/
Lc.length=La.length+Lb.length;
Lc.elem=new ElemType[Lc.length];
ElemType *pc=Lc.elem;
ElemType *pa=La.elem;
ElemType *pb=Lb.elem;
ElemType *pal=La.elem+La.length-1;//指针不能用[]来移动
ElemType *pbl=Lb.elem+Lb.length-1;
while((pa<=pal)&&(pb<=pbl)){
if(*pa<*pb){
*pc++=*pa++;
}else{
*pc++=*pb++;
}
}
while(pa<=pal) *pc++=*pa++;
while(pb<=pbl) *pc++=*pb++;
/********** End **********/
}
有头结点一定要记得跳过
void reverse (LinkList L)
{
//逆置L指针所指向的单链表
/********** Begin **********/
if (L == nullptr || L->next == nullptr) {
// Empty list or only one element, nothing to reverse.
return;
}
LinkList prev = nullptr;
LinkList current = L->next;
LinkList next_node;
while (current != nullptr) {
next_node = current->next; // Store the next node temporarily.
current->next = prev; // Reverse the current node's pointer.
prev = current; // Move the 'prev' pointer one step forward.
current = next_node; // Move the 'current' pointer one step forward.
}
// Update the head of the list to point to the new first element.
L->next = prev;
/********** End **********/
}
for (int j = 0; j < i - 1; j++) { p = p->next; }
插入型这样一定是对的
会跑到要插入位置的前一个
int ListInsert(LinkList &L,int i,int e)
{
// 在带头结点的循环单链表L的第i个元素之前插入元素e
/********** Begin **********/
if (i < 1||i > 6) {
return 0; // Invalid insertion position.
}
LinkList p = L;
for (int j = 0; j < i - 1; j++) {
p = p->next;
}
LinkList s = new LNnode;
s->data = e;
s->next = p->next;
p->next = s;
return 1; // Insertion successful.
/********** End **********/
}
int ListInsert(DLinkList &L,int i, ElemType e)
{
/********** Begin **********/
if (i < 1) {
return 0; // Invalid insertion position.
}
DLinkList p = L;
for (int j = 1; j < i && p != nullptr; j++) {
p = p->next;
}
if (p == nullptr) {
return 0; // Insertion position is out of bounds.
}
DLinkList s = new DLinkNode;
s->data = e;
s->prior = p;
s->next = p->next;
if (p->next != nullptr) {
p->next->prior = s;
}
p->next = s;
return 1; // Insertion successful.
/********** End **********/
}
int ListDelete(DLinkList L,int i,ElemType &e) // 不改变L
{
// 在带头结点的双向链表L中,删除第i个元素,并由e返回其值
/********** Begin **********/
if (i <= 0) {
return 0; // Invalid deletion position.
}
int j = 0;
DLinkNode *p = L;
while (p != nullptr && j < i) {
j++;
p = p->next;
}
if (p == nullptr) {
return 0; // Deletion position is out of bounds.
}
DLinkNode *q = p->prior; // Node before the one to be deleted.
q->next = p->next;
if (p->next != nullptr) {
p->next->prior = q;
}
e = p->data;
free(p); // Free the memory occupied by the deleted node.
return 1; // Deletion successful.
/********** End **********/
}
LinkList pa,pb,pc;
pa=La;
pb=Lb;
pc=La;
Lc=La;
while(pa&&pb){
if(pa->data<pb->data){
pc->next=pa->next;
pc=pc->next;
pa=pa->next;
}else{
pc->next=pb->next;
pb=pb->next;
pc=pc->next;
}
}
pc->next=pa?pa:pb;
void MergeList(LinkList &La, LinkList &Lb, LinkList &Lc)
{
// 已知单链线性表La和Lb的元素按值非递减排列。
// 归并La和Lb得到新的单链线性表Lc,Lc的元素也按值非递减排列。
/********** Begin **********/
LinkList pa = La->next;
LinkList pb = Lb->next;
LinkList pc = Lc = La; // Lc用La的头结点
while (pa && pb)
{
if (pa->data <= pb->data)
{
pc->next = pa;
pc = pa;
pa = pa->next;
}
else
{
pc->next = pb;
pc = pb;//pc向前移动一个单位
pb = pb->next;
}
}
pc->next = pa ? pa : pb; // 将剩余的非空链表直接连接到Lc的尾部
delete Lb; // 释放Lb的头结点
/********** End **********/
}
LinkList merge_1(LinkList LA,LinkList LB)
{
//将两个采用头指针的循环单链表的首尾连接起来
/********** Begin **********/
if (LA == NULL || LA->next == LA) {
return LB;
}
if (LB == NULL || LB->next == LB) {
return LA;
}
LinkList L = LB->next;
LinkList LAFirst = LA->next;
LinkList LALast = LAFirst;
while (LALast->next != LA) {
LALast = LALast->next;
}
LB->next = LAFirst;
LALast->next = L;
return LB;
/********** End **********/
}
STUDENT one ,two;
int i = 1,a = 1,b;
one = L -> next -> data;
L = L->next->next;
while(L){
i++;
if(compare(L->data,one)>0){
two = one;
b = a;
one = L -> data;
a = i;
}else if(compare(L->data,two)>0){
two = L -> data;
b = i;
}
L = L -> next ;
}
printf("第%d个学生成绩是第一名:\n",a);
vi(one);
printf("\n");
printf("第%d个学生成绩是第二名:\n",b);
/**********定义Func()函数**********/
int Func(int a[],int start,int end)
{
/********** Begin **********/
if (start == end) {
return a[start];
}
int middle = (start + end) / 2;
int leftMax = Func(a, start, middle);
int rightMax = Func(a, middle + 1, end);
//经典的分治法
return max(leftMax, rightMax);
/********** End **********/
}
//
#include <iostream>
#include <string>
void display(const std::string& message = "hello") {
std::cout << message << std::endl;
}
int main() {
std::string input;
std::cout << "请输入一个字符串(不输入任何字符时输出'hello'):";
std::getline(std::cin, input);
if (input.empty()) {
display(); // 输出 "hello"
} else {
display(input); // 输出输入的字符串
}
return 0;
}
void conversion(unsigned n)
{
// 对于输入的任意一个非负10进制整数,打印输出与其等值的16进制数
/********** Begin **********/
SqStack S;
SElemType e;
InitStack(S);
while(n){
Push(S,n%16);
n=n/16;
}
while(!StackEmpty(S)){
Pop(S,e);
if(e==10){
cout<<'A';
}else if(e==11){
cout<<'B';
}else if(e==12){
cout<<'C';
}else if(e==13){
cout<<'D';
}else if(e==14){
cout<<'E';
}else if(e==15){
cout<<'F';
}else{
cout<<e;
}
}
/********** End **********/
}
//栈的应用,回文的判断
#include <iostream>
#include <stack>
#include <string>
using namespace std;
bool isPalindrome(string str) {
stack<char> charStack;
// 将字符串的每个字符入栈
for (char c : str) {
charStack.push(c);
}
// 从栈中弹出字符并与原字符串比较
for (char c : str) {
if (charStack.top() != c) {
return false; // 不是回文
}
charStack.pop();
}
return true; // 是回文
}
int main() {
string str;
cout << "请输入一个字符串: ";
cin >> str;
if (isPalindrome(str)) {
cout << str << " 是回文。" << endl;
} else {
cout << str << " 不是回文。" << endl;
}
return 0;
}
//递归逆转输出
void reverse()
{
/********** Begin **********/
int num;
cin >> num;
if (num == 0) {
return; // 终止递归
} else {
reverse(); // 递归调用
cout << num << " ";
}
/********** End **********/
}
三.栈和队列
栈和队列也是线性表。
后进先出:LIFO;先进先出:FIFO。
栈
顺序栈
typedef struct{
Type *base;//nullptr表示栈不存在
Type *top;
int stacksize;
}Sqstack;
//初始化
int InitStack(Sqstack &s){
s.base=new Type[100];//第一步一定是分配大小。
if(!s.base) exit(OVERFLOW);
s.top=s.base;
s.stacksize=100;
return OK;
}
//入栈
int Push(SqStack &s,Type e){
if(s.top-s.base==s.stacksize) return ERROR;//因为是顺序栈,所以才可以这样
*s.top++=e;
return OK;
}
//出栈
int Pop(SqStack &s,Type e){
if(s.top==s.base) return ERROR;
e=*--s.top;
}
//栈顶元素
Tpye GetTop(Sqstack s){
if(S.top!=S.base)
return *(s.top-1);
}
链栈
typedef struct{
Type data;
struct StackNode *next;
}StackNode,*LinkStack;
//初始化
int InitStack(LinkSatck &s){
s=nullptr;
return OK;
}
//入栈
int Push(LinkStack &s,Type e){
p=new StackNode;
p->data=e;
p->next=s;
s=p;//头插法
return OK;
}
//出栈
int Pop(LinkStack &s,Type e){
if(s=nullptr) return ERROR;
Type e=s->data;
StackNode *p=s;
s=s->next;//相当于链表的头是栈顶
delete p;//当是数组时,delete[] arr;
return OK;
}
//取栈顶元素
Type GetTop(LinkStack S){
if(s!=nullptr){
return s-data;
}
}
递归
递归是一种标准的分值法求解
int m=0;
void move(char A,int n,char C){
cout<<++m<<","<<n<<","<<A<<","<<C<<endl;
}
//Hanoi塔的递归
void Hanoi(int n,char A,char B,char C){//中间的编号是被借助对象。
if(n==1) move(A,1,C);//将编号为1的从A移到C
else{
Hanoi(n-1,A,C,B);
move(A,n,C);
Hanoi(n-1,B,A,C);
}
}
Hanoi塔问题,斐波那契数列的时间复杂度O(2^n);空间复杂度:O(n)
队列
循环队列
typedef struct{
Type *base;
int front;
int rear;//队列的顺序存储结构,就直接采用记录数组角标,查找更快
}SqQueue;
//初始化
int InitQueue(SqQueue &Q){
Q.base=new Type[100];//像这种有Type*的数据成员,第一步就是申请空间
if(!Q.base) exit(OVERFLOW);
Q.front=Q.rear=0;
return OK;
}
//求长度
int QueueLength(SqQuque Q){
return (Q.rear-Q.front+maxsize)%maxsize;
}
//入队
int EnQueue(SqQueue &Q,Type e){
if((Q.rear+1)%maxsize==Q.front) return ERROR;
Q.base[rear]=e;
Q.rear=(Q.rear+1)%maxsize;
return OK;
}
//出队
int DeQueue(SqQueue &Q,Type &e){
if(Q.front==Q.rear) return ERROR;
e=Q.base[front];
Q.front=(Q.front+1)%maxsize;
return OK;
}
//取队头元素
Type GetHead(SqQueue Q){
if(Q.front!=Q.rear){
return Q.base[Q.front];
}
}
Q.front==Q.rear;//队空的条件
(Q.rear+1)%maxsize==Q.front;//队满的条件
链队列
//数据类型
typedef struct{
Type data;
struct QNode *next;
}QNode,*QueuePtr;
//链表结构
typedef struct{
QueuePtr front;
QueuePtr rear;
}LinkQueue;
//初始化
int InitQueue(LinkQueue &Q){
Q.front=Q.rear=new QNode;//初始化第一步都是分配空间
Q.front=Q.rear=nullptr;
return OK;
}
//入队
int EnQueue(LinkQueue &Q,Type e){
QNode *p=new QNode;
p->data=e;
p->next=nullptr;
Q.rear->next=p;
Q.rear=p;
return OK;
}
//出队
int DeQueue(LinkQueue &Q,Type &e){
if(Q.front==Q.rear) return ERROR;
p=Q.front->next;//有头结点
e=p->data;
Q.front->next=p->next;
if(p==Q.rear) Q.rear=Q.front;//如果最后一个元素被删,则把队置空
delete p;
return OK;
}
//取队头元素
Type GetHead(LinkQueue Q){
if(Q.front!=Q.rear)
return Q.front->next->data;//指针有点多,分清楚:指队头,指第一个有效结点,指数据部分
}
数制转换
void conversion(int N){
InitStack(s);
while(N){
Push(s,N%8);
N=N/8;
}
while(!StackEmpty){
Pop(s,e);
cout<<e;
}
}
时间复杂度,空间复杂度:O(log8n)
括号匹配
bool Matching(){
InStack(s);
flag=1;//一开始默认括号匹配成功
cin>>ch;
while(ch!="#"&&flag){
switch(ch){
case'[':
case'(':
Push(s,ch);
break;
case']':
if(!StackEmpty(s)&&GetTop(s)=='[') Pop(s,x);
else flag=0;
break;
case')':
if(!StackEmpty(s)&&GetTop(s)=='(') Pop(s,x);
else flag=0;
break;
}
cin>>ch;
}
if(StackEmpty(s)&&flag) return true;
else return false;
}
时间复杂度,空间复杂度:O(n)
栈可以在程序设计语言中实现递归
表达式求值
char EvaluateExperssion(){
InitStack(OPND);//操作数:operand
InitStack(OPTR);//运算符:operator
Push(OPTR,"#");
cin>>ch;
while(ch!="#"||GetTop(OPTR)!="#"){
if(!In(ch)){Push(OPND,ch);cin>>ch;}//不是运算符就进
else
switch(Precede(GetTop(OPTR),ch)){
case'<':
Push(OPTR,ch);
cin>>ch;
case'>':
Pop(OPTR,theta);
Pop(OPND,b);Pop(OPND,a);
Push(OPND,Operate(a,theta,b));
break;
case'=':
Pop(OPTR,x);cin>>ch;
break;
}
}
return GetTop(OPND);
}
时间复杂度,空间复杂度:O(n)
课后习题
长度计算:(r-f+n)%n,注意是末-初就可以了
计算fact(n)的递归要调用n+1次,以fact(0)为参照就做出来了
一个栈以向量v[1***n]存储,一开始top=n+1,元素进栈的操作:top–;V[top]=x;此处是高端进栈,就该这样
迭代是环结构,递归是树结构,一个递归算法就应该包含终止条件与递归部分
用链接的方式存储的队列,在进行删除运算时,头和尾指针都有可能被修改。因为如果是最后一个元素被删,那么也要q.front=q.rear;修改尾指针使其置空。
精选习题
/*****顺序栈的基本操作*****/
void InitStack(SqStack &S)
{
// 构造一个空栈S
S.base=new SElemType[STACK_INIT_SIZE];
if(!S.base){
exit(OVERFLOW);
}else{
S.top=S.base;
S.stacksize=STACK_INIT_SIZE;
}
}
void DestroyStack(SqStack &S)
{
S.stacksize=0;
delete[] S.base; //销毁一定要使其空间释放,然后再使其指针有个指向。
S.top=S.base=nullptr;
}
void ClearStack(SqStack &S)
{
delete[] S.base; // 释放内存 , e=*--S.top; . -- * 这个顺序
InitStack(S); // 重新初始化栈,很聪明,并不会一个一个去删
}
void StackTraverse(SqStack S,void(*visit)(SElemType))
//这是一个函数指针的声明,用来声明一个指向函数的指针。
{
SElemType *temp=S.base;
while(temp!=S.top){
visit(*temp);//调用函数visit()
temp++;
}
}
/*****链栈的基本操作*****/
void StackTraverse(LinkStack S, void(*visit)(SElemType))
{
LinkStack reverseStack;
InitStack(reverseStack);
LinkList p = S;
while (p)
{
Push(reverseStack, p->data); //逆序的方法:将元素压入辅助栈以逆序保存
p = p->next;
}
while (!StackEmpty(reverseStack))
{
SElemType e;
Pop(reverseStack, e);
visit(e);
}
cout<<endl;
DestroyStack(reverseStack);
}
/*****表达式的匹配*****/
int Match(char exp[],int n) //exp存放表达式,n是表达式长度
{
LinkStack S; //定义一个链栈st
InitStack(S); //栈初始化
int i=0;
char ch;
SElemType e;
while(i<n){
ch = exp[i];
switch(ch){
case'[':
case'(':
Push(S,ch);
break;
case')':
if(StackEmpty(S)) return false;
Pop(S,e);
if (e != '(') return false;
break;
case']':
if(StackEmpty(S)) return false;
Pop(S,e);
if (e != '[') return false;
break;
}
i++;
}
return true;
}
/*****队列的基本操作*****/
struct SqQueue //顺序队列
{
QElemType *base; // 初始化的动态分配存储空间
int front;
int rear; // 尾指针,若队列不空,指向队列尾元素的下一个位置
};
//Q.base = (int *)malloc(MAX_QSIZE * sizeof(int));一般都是初始化的时候分配空间。
void ClearQueue(SqQueue &Q)
{ // 将Q清为空队列
Q.front = Q.rear = 0; //指向相同即可,不用管里面的元素值以及空间释放等问题。
}
// return (Q.rear - Q.front + MAX_QSIZE) % MAX_QSIZE;
int EnQueue(SqQueue &Q, QElemType e)
{ // 插入元素e为Q的新的队尾元素
if ((Q.rear + 1) % MAX_QSIZE == Q.front)
{
return ERROR; // 都应该先判断,队列满。
}
Q.base[Q.rear] = e;
Q.rear = (Q.rear + 1) % MAX_QSIZE; //都是这种处理方式。
return OK;
}
int GetHead(SqQueue Q, QElemType &e)
{ // 区分返回队头元素和使队头元素出列。
if (Q.front == Q.rear) return ERROR;
e = Q.base[Q.front];
return OK;
}
四.串、数组和广义表
子串在主串中的位置以子串的第一个字符的位置来表示,区分空格串与空串
串的存储结构
typedef struct{
char ch[maxsize+1];
int length;
}SString;//顺序串,下标为0时不用
//堆式顺序存储结构
typedef struct{
char *ch;
int length;
}HString;//heap string
//链式存储结构
typedef struct {
char ch[10];//以10为单位连在一起
struct Chunk *next;//chunk:大块
}Chunk;
typedef struct{
Chunk *head,*tail;
int length;
}LString;//link string
BF算法
//Brute Force
int Index_BF(SString S,SString T,int pos){//从第pos个字符开始查找
int i=pos;
int j=1;
while(i<=S.length&&j<=length){
if(S.ch[i]==T.ch[j]){++i;++j;}
else{i=i-j+2;j=1;}//指针后退,重新开始匹配,i-j+1是回到起点,+1就是下一个位置
}
if(j>T.length) return j-T.length;//匹配成功,返回位置
else return 0;
}
自我验证:最好情况时间复杂度:O(m+n);最坏情况的时间复杂度:O(m*n)
KMP算法(impotant)
主串没有回溯
int Index_KMP(SString S,SString T,int pos){
int i=pos;
int j=1;
while(i<S.length&&j<T.length){
if(j==0||S.ch[i]==T.ch[j]){
i++;
j++;//如果匹配成功,就继续比较下一个字符
}else{
j=next[j];//匹配不成功就转到next[j]去比较
}
}
if(j>T.length) return i-T.length;//如果超出长度,则说明匹配成功了,返回其起始位置
else return 0;
}
前缀:包含首位但不包含末位;后缀:包含末位但不包含首位
//计算next[]的值
void get_next(SString T,int next[]){
int i=1,j=0;
next[1]=0;
while(i<=T.length){//循环n次,为这n个数匹配next[]值
if(j==0||T.ch[i]==T.ch[j]){
i++;
j++;
next[i]=j;//起始时j=0,next[2]=1
}else{
j=next[j];
}
}
}
//nextval[]的值,及next[]的修正值
void get_nextval(SString T,int nextval[]){
int i=1,nextval[1]=0;
int j=0;
while(i<=T.length){
if(j==0||T.ch[i]==T.ch[j]){
i++;
j++;
if(T.ch[i]!=T.ch[j]) nextval[i]=j;
else nextval[i]=nextval[j];
}else{
j=nextval[j];
}
}
}
最后一位并不会影响next[ ]的结果。
拷贝的是nextval[ ]的值而非next[ ]的值。
当a=a时,拷贝nextval[ ],否则拷贝自身的next[ ]
KMP算法当且仅当有很多部分匹配的时候,才比BP算法快得多。
时间复杂度:O(m+n)
广义表与数组
数组的存储结构:
Loacte(i,j)=LOC(0,0)+(n*i+j)L
压缩存储,是指为多个值相同的元只分配一个存储空间,对零元不分配存储空间
上三角矩阵:K=(i-1)(2n-i+2)/2+(j-i);i<=j n(n+1)/2;i>j
广义表:
广义表分为原子与子表 GetTail(LS)=( )//(())二者不同length=0,1
取表尾GetTail(LS):去除表头之外的元素构成的表,所以表尾一定是个广义表
typedef enum{ATOM,LIST} ElemTag;//ATOM==0,LIST==1
typedef struct GLNode{
ElemTag tag;
union{
Type atom;
struct{
struct GLNode *hp,*tp;//指向表头与表尾
}ptr;//指针的一个集合
};
}*GList
(A(B(E(K,L),F),C(G),D(H(M),I,J)))
最高层表的结点个数就是广义表的长度
表头,及第一个元素,可以是单原子,也可以是一个子表。
病毒感染检测
void Virus_detection(){
ifstream inFile("输入.txt");
ofstream outFile("输出.txt");
inFile>>num;
while(num--){
inFile>>Virus.ch+1;//读入病毒DNA序列,从下标为1开始存放
inFile>>Person.ch+1;
Vir=Virus.ch;
int flag=0;//暂时未匹配成功
m=Virus.length;
for(int i=m+1,j=1;;j<=m;j++)
Virus.ch[i++]=Virus.ch[j];
Virus.ch[2*m+1]='\0';//长度扩大为两倍,可以实现循环m次的序列
for(int i=0;i<m;i++){
for(int j=1;j<=m;j++)
temp.ch[j]=Virus.ch[i+j];//循环m次的序列
temp.ch[m+1]='\0';
flag=Index_BF(Person,temp,1);
if(flag) break;
}
if(flag) outFile<<Vir+1<<" "<<Person.ch+1<<" "<<"YES"<<endl;
else outFile<<Vir+1<<" "<<Person.ch+1<<" "<<"NO"<<endl;
}
}
BF算法:O(mn),但由于是环形,则O(mmn),再由于有num个,所以整个的时间复杂度为O(num-m-m-n)
课后习题
线性表可以看成广义表的特例,里面的union只有一种类型
数组不是同类型值的集合
子串是主串中任意个连续字符组成的序列,且包含自身与空=1+2+3+***+n
广义表的深度是数括号的个数
EX.行-19,列111,A[7,4]起始,-1~6有8个数,8*11+3=91,说白了注意起始非0
当说是下标时,就是+j,当说是地址时,由于是间隔,就是+(j-1)
五.树和二叉树
结点的度:结点拥有的子树数;叶子的度为零。
除根结点以外,非终端结点也称为内部结点。
有序树:子树从左到右是有顺序的,不能互换位置。
森林:n个互不相交的树的集合,对于树每一个结点而言,其子树的集合就是森林。
二叉树:最大的度为2,且有左右之分。
哈夫曼树:左0右1
二叉树基本操作
ADT BinaryTree{
InitBiTree(&T);
DestroyBiTree(&T);
CreateBiTree(&T,definition);
ClearBiTree(&T);//将二叉树清为空树
BiTreeEmpty(T);
BiTreeDepth(T);
Root(T);//返回根
Value(T,e);//返回某个结点的值
Assign(T,&e,value);//结点e赋值value
LeftSibling(T,e);//返回左兄弟,不然返回空
PreOrderTraverse(T);//波兰式
InOrderTraverse(T);
PostOrderTraverse(T);//逆波兰式
LevelOrderTraverse(T);//层序遍历
InsertChild(&T,p,LR,C);//根据LR=0/1,插入C作为p所指结点的左子树或者右子树
}
二叉树性质:
非叶子结点数=n/2(向下取整);叶子结点数=n-n/2(向下取整)
第i层最多2(i)-1个结点,深度为k的树最多有2(k)-1个结点
满二叉树:度数都为2;完全二叉树:与相同层数的满二叉树的编号相同
叶子只有可能在最大的两层出现
对于任意结点,右子树max层=i,那么左子树max层=i/i+1
n个结点的完全二叉树的深度:log2n(向下取整)+1
结点标号为i,双亲:i/2(向下取整),左孩子2i(如果>n,则没有),左孩子2i+1(如果>n,则没有)
顺序存储结构:12345000067,适用于完全二叉树,但空间会极大浪费
遍历二叉树及其应用
typedef struct BiTNode{
Type data;
struct BiTNode *lchild,*rchild;
}BiTNode,*BiTree;//记住实质上是一个指针
遍历的实质是对数的线性化
//递归算法
void InOrderTraverse(BiTree T){
if(T){
InOrderTraverse(T->lchild);
cout<<T->data;
InOrderTraverse(T->rchild);
}
}
//非递归算法
void InOrderTraverse(BiTree T){
InitStack(s);
BiTNode *p=T;
while(p||!StackEmpty(S)){//还没遍历完和还没输出完都不应该出循环
if(p){//当为nullptr时,则说明左边走到底了
Push(s,p);
p=p->lchild;//其实while就是一个递归,为下一次做准备
}else{
Pop(s,q);
cout<<q->data;
p=q->rchild;//当p为左底时,输出左底,没有右孩子,则q为nullptr,注意q没进栈,这次弹出的是p的双亲,输出双亲,再遍历右孩子,则逻辑正确,中间的结点更好理清。
}
}
}
时间复杂度,空间复杂度:O(n)
//先序生成树
void CreateBiTree(BiTree &T){
cin>>ch;
if(ch=='#') T=nullptr;
else{
T=new BiTNode;
T->data=ch;
CreateBiTree(T->lchild);//有两个#说明是叶子
CreateBiTree(T->rchild);//ex.ABC##DE#G##F###
}
}
//复制二叉树
void Copy(BiTree T,BiTree &NewT){
if(T==nullptr){
NewT=nullptr;
return;
}else{
NewT=new BiTNode;
NewT->data=T->data;
Copy(T->lchild,NewT->lchild);
Copy(T->rchild,NewT->rchild);
}
}
//计算深度
int Depth(BiTree T){
if(T==nullptr) return 0;//千万别忘最底层使其回溯的判断语句,不然一直运行,什么都不输出
else{
m=Depth(T->lchild);
n=Depth(T->rchild);
if(m>n) return (m+1);
else return (n+1);
}
}
//统计结点个数
int NodeCount(BiTree T){
if(T==nullptr) return 0;
else return NodeCount(T->lchild)+NodeCount(T->rchild)+1;
}
线索二叉树
n个结点的二叉链表中必定存在n+1个空链域:2n-(n-1)=n+1
0:孩子,1:前驱后继
typedef struct BiThrNode{
Type data;
struct BiThrNode *lchild,*rchild;
int LTag,RTag;
}BiThrNode,*BiThrTree;
(虚线为线索,中序序列的第一个结点的lchild和最后一个结点的rchild均指向头结点)参考图详见手机
//注意以线性存储来理解,不然很不好理解
void InThreading(BiThrTree p){
if(p){
InThreading(p->lchild);
if(!p->lchild){
p->LTag=1;
p->lchild=pre;
}else p->LTag=0;
if(!pre->rchild){
pre->RTag=1;//一开始pre指向头结点
pre->rchilld=p;//相当于左右互指
}else pre->RTag=0;
pre=p;//记住更新pre
InThreading(p->rchild);
}
}
//带头节点的二叉树中序线索化
void InOrderThreading(BiThrTree &Thrt,BiThrTree T){//Thrt指向头结点
Thrt=new BiThrNode;
Thrt->LTag=0;//左孩子为树根
Thrt->RTag=1;//右孩子为线索
Thrt->rchild=Thrt;//一开始指向自己
if(!T) Thrt->lchild=Thrt;//如果树为空,左指针也指向自己
else{
Thrt->lchild=T;
pre=Thrt;//pre的初始化很重要
InThreading(T);
pre->rchild=Thrt;//算法结束时,pre为最右结点,pre的右线索指向头结点。
pre->RTag=1;
Thrt->rchild=pre;//双向指针,注意两边都要改
}
}
//遍历中序线索二叉树
void InOrderTraverse_Thr(BiThrTree T){
p=T->lchild;//指向根结点
while(p!=T){//空树或者遍历完时指向T
while(p->LTag==0) p=p->lchild;//中序都是左边走到头
cout<<p->data;//访问左孩子为空的最左端结点
while(p->RTag==1&&p->rchild!=T){
p=p->rchild;
cout<<p->data;//沿着右线索继续访问,就不在使用中序法,不用回溯了,速度更快
}
p=p->rchild;//因为上一个while的结束条件是p->RTag==0,则有右孩子,左边以及根输出完了,就该走到右子树,再以其为根,重新进入大while循环。
}
}
中序:前驱为左子树的最右端,后继为右子树的最左端
先序:若*p是双亲的左孩子,前驱为其双亲结点,否则是其双亲的左子树的最后遍历到的结点;后继为左子树根(若存在)或其右子树根
引入二叉树的目的是加快查找前驱和后继的速度
在一棵有n个度为k的结点的树,必有nk-(n-1)=n(k-1)+1个空链域
树的存储结构:双亲表示法:记录双亲的索引;孩子表示法:以孩子的索引为数据元素,以链表的形式串起来,还可以合并为带双亲的孩子链表
孩子兄弟表示法:
typedef struct{
Type data;
struct CSNode*firstchild,*nextsibling;
}CSNode,*CSTree;
森林和树的交换:
森林:F={T1,T2,T3,***,Tm} 孩子往左,兄弟往右
哈夫曼树
又称最优树,树的路径长度:从树根到每一结点的路径长度之和
树的带权路径长度(WPL):所有叶子结点的带权路径长度之和
典型的贪心算法,2n-1个结点
构造哈夫曼树:
void CreateHuffmanTree(HuffmanTree &HT,int n){
if(n<=1) return;
m=2*n-1;
HT=new HTNode[m+1];//0单元不用
for(int i=1;i<=m,i++){
HT[i].parent=0;
HT[i].lchild=0;
HT[i].rchild=0;
}
for(int i=1;i<=m,i++){
cin>>HT[i].weight;
}
for(int i=n+1;i<=m;i++){
Select(HT,i-1,s1,s2);//s1,s2为权值最小的两个结点
HT[s1].parent=i;
HT[s2].parent=i;
HT[i].lchild=s1;
HT[i].rchild=s2;
HT[i].weight=HT[s1].weight+HT[s2].weight;
}
}
//选择权重最小的两个节点
void Select(HuffmanTree HT, int i, int &s1, int &s2) {
int min1 = -1, min2 = -1; // 初始化这里我当时出问题了。
for (int j = 1; j <= i; j++) {
if (HT[j].parent == 0) {
if (min1 == -1 || HT[j].weight < HT[min1].weight) {
min2 = min1;
min1 = j; // 还没赋值就赋值
} else if (min2 == -1 || HT[j].weight < HT[min2].weight) {
min2 = j;
}
}
}
s1 = min1;
s2 = min2;
}
左0右1,前缀编码不产生二义性,且哈夫曼树是最优的前缀编码
前n为叶子结点,后n-1为非叶子结点
构造哈夫曼编码:
在通信编码技术上具有广泛应用
//构造编码
void HuffmanCoding(HuffmanTree HT,HuffmanCode &HC,int n) //建立哈夫曼树编码
{ // 根据哈夫曼树HT,求出n个字符的赫夫曼编码HC
HC=new char*[n+1]; // n+1 个char 类型的指针。
char *cd=new char[n];// 临时存放单元
cd[n-1]='\0';
for(int i=1;i<=n;i++){
int start=n-1;
int c=i;
int f=HT[i].parent;
while(f!=0){ // 上溯直到根节点
--start;
if(HT[f].lchild==c){
cd[start]='0'; // 遵循左0右1的原则
}else{
cd[start]='1';
}
c=f;f=HT[f].parent; // 开始进入循环,逐渐向上递归寻找。
}
int size=n-start;
HC[i]=new char[size];
strcpy(HC[i],&cd[start]);
}
delete cd; // 删除暂存空间。
}
表达式求值
当遇到运算符时不能直接创建结点,应该跟前面的运算符的优先级比较
void InitExpTree(){
InitStack(EXPT);
InitStack(OPTR);
Push(OPTR,'#');
cin>>ch;
while(ch!='#'||GetTop(OPTR)!='#'){
if(!In(ch)){
CreateExpTree(T,nullptr,nullptr,ch);//创建以操作数为基准的二叉树
Push(EXPT,T);//将二叉树的根结点进栈
cin>>ch;
}else{
switch(Precede(GetTop(OPTR,ch))){
case'<':
Push(OPTR,ch);cin>>ch;//不要忘记要继续承接
case'>':
Pop(OPTR,theta);
Pop(EXPT,b);
Pop(EXPT,a);
CreateTxpTee(T,a,b,theta);
Push(EXPT,T);//更新二叉树,重新进入
break;
case'=':
Pop(OPTR,x);cin>>ch;
break;
}
}
}
}
//表达式求值
int EvaluateExpTree(BiTree T){
int lvalue=rvalue=0;
if(T->lchild==nullptr&&T->rchild==nullptr) return T->data-'0';//说明最终只剩下第一棵树,现在以数字的形式返回
else{
lvalue=EvaluateExpTree(T->lchild);
rvalue=EvaluateExpTree(T->rchild);
return GetValue(T->data,lvalue,rvalue);//操作符,左操作数,右操作数
}
}
小结:
顺序存储更适合完全二叉树
先序,中序,后序时间复杂度都为O(n)
课后习题
把一棵树转化为二叉树,这棵树的形态是唯一的。
F是森林,B是其转化而来的二叉树,若F中有n个非终端结点,则B中右指针域为空的结点有(n+1)个
树的存储结构表示法没有顺序存储表示法
在没有表明是线索二叉树时,根结点的右指针为nullptr
有3个结点可以创建出5种不同的二叉树
一个具有1025个结点的树高为11~1025
在一棵度为4的树中,degree=4(20),degree=3(10),degree=2(1),degree=1(10),则叶子结点的个数为82。
精选习题
// 非递归遍历中序树
void InOrderTraverse2(BiTree T,void(*Visit)(TElemType))
{ // 采用二叉链表存储结构,Visit是对数据元素操作的应用函数。
// 中序遍历二叉树T的非递归算法,对每个数据元素调用函数Visit
/********** Begin *********/
SqStack s;
BiTree p=T;
BiTNode *q=new BiTNode;
InitStack(s);
while(!StackEmpty(s)||p){
if(p){
Push(s,p);
p=p->lchild;
}
else{
Pop(s,q);
Visit(q->data);
p=q->rchild;
}
}
// 层次法遍历树
void LevelOrderTraverse(BiTree T,void(*Visit)(TElemType)) {
/********** Begin **********/
LinkQueue queue;
InitQueue(queue);
if (T) {
EnQueue(queue,T);
}
while(!QueueEmpty(queue)){
int n=QueueLength(queue);
for(int j=0 ; j<n; j++){
QElemType temp;
DeQueue(queue,temp);
cout<<temp->data<<",";
if (temp->lchild!=nullptr)
EnQueue(queue,temp->lchild);
if(temp->rchild!=nullptr)
EnQueue(queue,temp->rchild);
}
}
}
// 实现二叉树左右子树的交换(非递归的栈实现)
void exchange(BiTree T) {
//实现二叉树左右子树的交换 (栈实现)
SqStack stack;
Initstack(stack);
Push(stack,T);
while (!stackEmpty(stack)) {
BiTree temp;
int a = Pop(stack,temp);
BiTree t = temp->lchild;
temp->lchild = temp->rchild;
temp->rchild = t;
if (temp->lchild) Push(stack, temp->lchild);
if (temp->rchild) Push(stack, temp->rchild);
}
}
六.图
图的定义
G=(V,E) 有序:<x,y> 无序:(x,y)
如果边和点都是子集,则G‘是G的子图 度=入度+出度
网=图+权 无向完全图,有向完全图:n(n-1)条边
边数等于所有点的度数/2 不重复出现的边的路径为简单路径
连通图是一个整体,连通分量是极大连通子图,在有向图中,往返都有路径,则为强连通图
连通图的生成树:含有图的全部顶点,但有足以构成一棵树的n-1条边 生成森林
六度空间理论:150^6
V:点集;v:点;VR:弧集;u:特征
//基本操作
ADT Graph{
FirstAdjVex(G,v);//返回邻接顶点
NextAdjVex(G,v,w);//返回相对于v,w的邻接顶点
InsertArc(&G,v,w)//增添弧v->w
}
邻接矩阵(Adjacency Matrix):无向图具有对称性,填0或1;如果是网,则填边权和+无穷
对于无向图,第i行元素的和就是顶点vi的度;有向图:行为出度,列为入度
#define MaxInt 32767
#define MaxNum 100
typedef char VerTexType
typedef int ArcType
typedef struct{
VerTexType vex[MaxNum];
ArcType arcs[MaxNum][MaxNum];
int vexnum,arcnum;
}
//创建无向网G,UDN超密集网络
int CreateUDN(AMGraph &G){
cin>>G.vexnum>>G.arcnum;
for(int i=0;i<G.vexnum;i++){
cin>>G.vexs[i];
}
for(int i=0;j<G.vexnum;i++){
for(int j=0;j<G.vexnum;j++){
G,arcs[i][j]=MaxInt;
}
}
for(int k=0;k<G.arcnum;++k){//更新这arcnum个关系
cin>>v1>>v2>>w;//输入依附的顶点与权值
i=LocateVex(G,v1);
j=LocateVex(G,v2);
G.arcs[i][j]=w;
G.arcs[j][i]=G.arcs[i][j];
}
return OK;
}
邻接表
表头结点:data 表尾节点:adjvex info nextare
在有向图中,第i个链表中结点的个数就是顶点Vi的出度,逆领接表只有有向图才会有
#define MVNum 100
typedef struct ArcNode{
int adjvex;
struct ArcNode *nextarc;
OtherInfo info;
}ArcNode;
typedef struct VNode{
VexTexType data;
ArcNode *firstarc;//第一条依附于该点的边
}VNode,AdjList[MVNum];
typedef struct{
AdjList vertices;
int vexnum,arcnum;
}ALGraph;
采用邻接表创建无向图
int CreatUDG(ALGraph &G){//创建无向图
cin>>G.vexnum>>G.arcnum;
for(int i=0;i<G.vexnum;i++){
cin>>G.vertices[i].data;
G.vertices[i].firstarc=nullptr;
}
for(int k=0;k<G.arcnum;k++){
cin>>v1>>v2;
i=LocateVex(G,v1);j=LocateVex(G,v2);
ArcNode *p1=new ArcNode;
p1->adjvex=j;//终点为j
p1->nextarc=G.vertices[i].firstarc;
G.vertices[i].firstarc=p1;
ArcNode *p2=new ArcNode;
p2->adjvex=i;
p2->nextarc=G.vertices[j].firstarc;
G.vertices[j].firstarc=p2;//因为是无向图,所以两边都要相互依附
}
}
一个图的邻接矩阵式唯一的,但其邻接表表示不唯一。
邻接表或逆邻接表适合表示稀疏图,稠密图常采取邻接矩阵表示法。
十字链表(Orthogonal List)是有向图的一种表示方法:
tailvex headvex hlink tlink info data firstin firstout
#define MAX_VERTEX_NUM 20
typedef struct ArcBox{
int tailvex,headvex;
struct ArcBox *hlink,*tlink;
InfoType *info;
}ArcBox;
typedef struct VexNode{
VertexType data;
ArcBox *firstin,*firstout;
}VexNode;
typedef struct{
VexNode xlist[MAX_VERTEX_NUM];//表头向量
int vexnum,arcnum;
}OLGraph;
邻接多重表(Adjacency Multilist)针对无向图
#define MAX_VERTEX_NUM 20
typedef enum{unvisited,visited} VisitIf;
typedef struct EBox{
VisitIf mark;
int ivex,jvex;//头点,尾点
struct EBox *ilink,*jlink;//共头边,共尾边
InfoType *info;
}Ebox;
typedef struct VexBox{
VertexType data;
EBox *firstedge;//注意指针都是这种类型
}VexBox;
typedef struct{
VexBox adjmulist[MAX_VERTEX_NUM];
int vexnum,edgenum;
}AMLGraph;
搜索与遍历
辅助数组:visited[n] false/0,表示没有被访问到,true则访问到。
DFS
是先序遍历的推广
广度优先生成树,深度优先生成树
bool visited[MVNum]
void DFS(Graph G,int v){
cout<<v;visited[v]=true;
for(w=FirstAdjVex(G,v);w>=0;w=NextAdjVex(G,V,W))//w>=0表示还存在此点
if(!visited[w]) DFS(G,w);
}
void FirstAdjVex(Graph G,int v){
int i;
for(i=1;i<=G.vexnum;i++){
if(G.arc[v][i].adj!=0){//说明是邻接点,有边存在
return i;
}
}
return -1;
}
void NextAdjvex(Grapg G,int v,int w){
int i;
for(int i=w+1;i<=G.vexnum;i++){//由于FirstAdjVex的寻找方法,w之前的不可能再有了,就接着往后面找
if(G.arc[v][i].adj!=0){//说明是邻接点,有边存在
return i;
}
}
return -1;
}
如果是非连通图,那么每个闭环都要作为单位单独调用使用
深度优先遍历
void DFSTraverse(Graph G){
for(int v=0;v<G.vexnum;v++) visited[v]=false;
for(int v=0;v<G.vexnum;v++)
if(!Visited[v]) DFS(G,v);
}
//采用邻接矩阵
void DFS_AM(AMGraph G,int v){
cout<<v;visited[v]=true;
for(int w=0;w<G.vexnum;w++){
if((G.arcs[v][w]!=0)&&(!visited[w])) DFS_AM(G,w);
}
}
//采用邻接表
void DFS_AL(ALGraph G,int v){
cout<<v;visited[v]=true;
p=G.vertices[v].firstarc;
while(p!=nullptr){
w=p->adjvex;
if(!visited[w]) DFS_AL(G,w);
p=p->nextarc;
}
}
邻接矩阵:O(n^2) 邻接表:O(n+e)
BFS
先访问的结点其邻接点亦先被访问,BFS与队列是天生一对
void BFS(Graph G,int v){
cout<<v;visited[v]=true;
InitQueue(Q);
EnQueue(Q,v);
while(!QueueEmpty(Q)){
DeQueue(Q,u);//队头元素出队并置为u
for(w=FirstAdjVex(G,v);w>=0;w=NextAdjVex(G,V,W))
if(!visited[w]){
cout<<w;
visited[w]=true;
EnQueue(Q,w);
}
}
}
最小生成树
n个结点的树一定只有n-1条边
最小代价生成树:简称最小生成树
Prim
加点法,重点是U,V-U
struct{
VerTexType adjvex;
ArcType lowcost;
}closeedge[MVNum];
void MiniSpanTree_Prim(AMGraph G,VerTexType u){
k=LoacteVex(G,u);
for(int j=0;j<G.vexnum;j++){
if(j!=k) closeedge[j]={u,G.arcs[k][j]};
}
closeedge[k].lowcost=0;
for(int i=1;i<G.vexnum;i++){
k=Min(closedge);
u0=closedge[k].adjvex;
v0=G.vexs[k];
cout<<u0<<v0;
closedge[k].lowcost=0;//将第K个顶点并入U集
for(int j=0;j<G.vexnum;++j)
if(G.arc[k][j]<closedge[j].lowcost)
closedge[j]={G.vex[k],G.arcs[k][j]};
}//U==V,边集等于网点集
}
时间复杂度:O(n^2),与边数无关,用于稠密网
Kruskal
必须是将不同的连通分量相连,加边法
struct {
VerTexType Head;
VerTexType Tail;
ArcType lowcost;
}Edge[arcnum];
//连通分量
int Vexset[MVNum];
//算法
void MiniSpanTree_Kruskal(AMGraph G){
Sort(Edge);
for(int i=0;i<G.vexnum;i++)
Vexset[i]=i;
for(int i=0;i<G.arcnum;i++){
v1=LocateVex(G,Edge[i].Head);
v2=LocateVex(G,Edge[i].Tail);
vs1=Vexset[v1];
vs2=Vexset[v2];
if(vs1!=vs2){
cout<<Edge[i].Head<<Edge[i].Tail;
for(int j=0;j<G.vexnum;j++)
if(Vexset[j]==vs2) Vexset[j]=vs1;//合并编号
}
}
}
O(elog2e) 与边数有关,适合求稀疏网的最小生成树
Dijkstra
最短路径问题:第一个结点为源点,最后一个结点为终点,Dijkstra针对单源点问题。
从V0到Vi,如果有弧,则Path[i]=V0,否则为-1,最短路径长度为D[i]
void ShortestPath_DIJ(AMGraph G,int v0){//求v0到其余顶点的最短距离
int n=G.vexnum;
for(int v=0;v<=n;v++){
s[v]=false;
D[v]=G.arcs[v0][v];//边的权值
if(D[v]<MAXInt) Path[v]=v0;//如果有弧,则设置其权值
else Path[v]=-1;
}
s[v0]=true;//将V0并入S
D[v0]=0;//源点到源点的距离为0
for(int i=1;i<n;i++){
int min=MaxInt;
for(int w=0;w<n;++w)
if(!s[w]&&D[w]<min){//在V-S中去找最小路径
v=w;//记录该点,准备把他并入S中
min=D[w];//找到小值就替换一次
}
s[v]=true;//使其并入s中
for(int w=0;w<n;w++){//加点不是目的,关键是更新路径
if(!s[w]&&(D[v]+G.arc[v][w]<D[w])){//因为是v的并入才更新所以只需要判断中间数v的导入就可以了。
D[w]=D[v]+G.arc[v][w];
Path[w]=v;
}
}
}
}
0、3、4、5根据前驱反过来,就是dmin
时间复杂度为O(n^2)
Floyd
求得是每一对顶点的最短路径,使用O(n*n2)=O(n3)
void ShortestPath_Floyed(AMGraph G){
for(i=0;i<G.vexnum;i++)
for(j=0;j<G.vexnum;j++){
D[i][j]=arc[i][j];
if(D[i][j]<MaxInt && i!=j) Path[i][j]=i;//若有弧,则前驱设为i
else Path[i][j]=-1;
}
for(k=0;k<G.vexnum;++k)//更新k个点的路径
for(i=0;i<G.vexnum;i++)
for(j=0;j<G.vexnum;j++)
if(D[i][k]+D[k][j]<D[i][j]){
D[i][j]=D[i][k]+D[k][j];
Path[i][j]=Path[k][j];//因为从i到j中间要经过从k到j,则在到j的结尾的Path也就是Path[k][j]了
}
}
//Path[1][2]=3,则前驱为3,Path[1][3]=1,所以确定路线为{<1,3><3,2>}
AOV-网
DMG表示有向无环图,点之间是有逻辑顺序的。
用顶点表示活动,用弧表示活动间的优先关系的有向图:AOV-网
如果拓扑排序,所有顶点都在序列中,则必定不存在环
注意:同一个图的拓扑有序序列不唯一。
拓扑排序
int ToplogicalSort(ALGraph G,int topo[]){
FindInDegree(G,indegree);//找到每个点的入度,并存到数组中
InitStack(S);
for(i=0;i<G.vexnum;i++)
if(!indegree[i]) Push(S,i);//入度为0的进栈
m=0;
while(!StackEmpty(S)){
Pop(S,i);
topo[m]=i;//开始排序
++m;
p=G.vertices[i].firstarc;//指向第一个邻接点
while(p!=nullptr){
k=p->adjvex;
--indegree[k];
if(indegree[k]==0) Push(S,k);//说明先处理新的入度为0的点,再处理老入度为0的点
p=p->nextarc;
}
}
if(m<G.vexnum) return ERROR;
}
AOE-网
Activity On Edge Netword
注意里面是同时都要完成,是&&而不是||的关系
只有一个源点,和只有一个汇点
一条从源点到汇点的带权路径长度最长的路径,称为关键路径。因为完成得快没有用,需要等别人。
事件vi的最早发生时间:ve(i)=max{ve(k)+wk,i}
事件vi的最迟发生时间:vl(i)=min{vl(k)-wi,k}//最小对应最早
活动ai=<vj,vk>的最早开始时间e(i)=ve(j)
活动ai=<vj,vk>的最迟开始时间l(i)=vl(k)-wj,k
对于关键活动而言,e(i)=l(i),对于非关键活动,l(i)-e(i)是该工程的时间余量,在此范围的适当延误不会影响预期。
最早求max,最晚求min,因为是且不是或的关系
//关键路径算法
int CriticalPath(ALGraph G){
if(!TopologicalOrder(G,topo)) return ERROR;
n=G.vexnum;
for(i=0;i<n;i++)
ve[i]=0;//每个事件的最早发生时间初始化为0
for(i=0;i<n;i++){
k=topo[i];//获得拓扑序列原始的序号
p=G.vertices[k].firstarc;
while(p!=nullptr){
j=p->adjvex;
if(ve[j]<ve[k]+p->weight)
ve[j]=ve[k]+p->weight;//最早发生时间应该讲究最晚完成的人
p=p->nextarc;
}
}
for(i=0;i<n;i++)
vi[i]=ve[n-1];//初始化每个最迟发生的时间
for(i=n-1;i>=0;i--){//按照逆拓扑排序求每个事件的最迟发生时间
k=topo[i];
p=G.vertices[k].firstarc;
while(p!=nullptr){
j=p->adjvex;
if(vl[k]>vl[j]-p->weight)//能早就尽量早,不能太迟了。
vl[k]=vl[j]-p->weight;//你能做的最迟的时间,但不能耽搁后面的最迟时间。
p=p->nextarc;
}
}
for(i=0;i<n;i++){
p=G.vertices[i].firstarc;
while(p!=nullptr){
j=p->adjvex;
e=ve[i];
l=vl[j]-p->weight;
if(e==l)
cout<<G.vertices[i].data<<G.vertices[j].data;//输出此关键路径
p=p->nextarc;
}
}
}
六度空间理论
void SixDegree_BFS(Graph G,int Start){
visit_Num=0;//记录路径不超过7的个数
visit[Start]=true;
InitQueue(Q);
EnQueue(Q,Start);
level[0]=1;
for(len=1;len<=6&&!QueueEmpty(Q);len++){//关系拓展最多到六层
for(i=0;i<level[len-1];i++){
Dequeue(Q,u);//队头元素出队
for(w=FirstAdjvex(G,u);w>=0;w=NextAdjvex(G,u,w))
if(!visit[w]){
visit[w]=true;
visit_Num++;
level[len]++;//该层的顶点个数+1
EnQueue(Q,w);//相邻点作为拓展范围,继续入围
}
}
}
cout<<100*visit_Num/G.vexnum;//显示出百分比
}
选择题与判断题
采用不同的遍历方法,得到的无向图的生成树不是相同的
对于任意一个图,从它的某个结点进行一次深度或广度优先遍历可以访问到该图的每个顶点。(F)
如果是无向的连通图,或者有向的强连通图是对的,对于无向的非连通图不对,对于有向的非强连通图则有可能对,有可能不对。
在有向图中,所有顶点的入度之和等于所有顶点的出度之和。
一个图的深度优先搜索树是唯一的,而广度不唯一:广度优先遍历在采用邻接表存储图的情况下,由于边表序列的不唯一性,导致广度优
先遍历的结果也不唯一。
连通图上各边的权值不同,则该图的最小生成树是唯一的。(T)例如Kruskal算法,其中依次选权值最小的边,不会有抉择问题。
如果有向图中各个顶点的度都大于2,则该图中必有回路。(F )
邻接表和邻接矩阵对于有向图和无向图都适用。
已知无向图G含有16条边其中度为4的顶点个数为3,度为3的顶点个数为4,其他顶点的度均小于3。图G所含的顶点个数至少是(3+4+4=11)。
注意层次遍历也要按层次的顺序来。
Kruskal适合稀疏图采用加边法,与边数有关,Prim适合稠密图,因为他跟边数无关。
只要是DFS,就借助栈,而非什么邻接表还是矩阵无关。
深度生成树的树高比广度生成树的树高要高
无向图中一个顶点的度是指图中与该顶点相邻接的顶点数
课后习题
enum GraphKind{DG,DN,UDG,UDN}; // 有向图,有向网,无向图,无向网
void DestroyGraph(MGraph &G)
{ // 初始条件:图G存在。操作结果:销毁图G
/********** Begin **********/
for (int i = 0; i < G.vexnum; ++i) {
for (int j = 0; j < G.vexnum; ++j) {
delete[] G.arcs[i][j];
}
}
G.vexnum = 0;
G.arcnum = 0;
/********** End **********/
}
//针对邻接表
void DFS(ALGraph G,int v)
{ //从第v个顶点出发递归地深度优先遍历图G
/********** Begin **********/
ArcNode *p;
p=G.vertices[v].firstarc;
visited[v]=1;
visit(G.vertices[v].data);
for(int w=FirstAdjVex(G,G.vertices[v].data);w>=0;w=NextAdjVex(G,G.vertices[v].data,G.vertices[w].data)){
if(!visited[w])
DFS(G,w);
}
/********** End **********/
}
void BFSTraverse(ALGraph G)
{ //按广度优先非递归遍历图G。使用辅助队列Q和访问标志数组visited。
/********** Begin **********/
int v,u,w;
ArcNode *p;
SqQueue Q;
for(v=0;v<G.vexnum;v++)
visited[v]=0;
InitQueue(Q);
for(v=0;v<G.vexnum;v++)
if(!visited[v]){
visited[v]=1;
visit(G.vertices[v].data);
EnQueue(Q,v);//初始化,第一个进queue
while(!QueueEmpty(Q)){
DeQueue(Q,u);
p=G.vertices[u].firstarc;
while(p){
w=p->data.adjvex;//相互依赖的边的一个顶点
if(!visited[w]){
visited[w]=1;
visit(G.vertices[w].data);
EnQueue(Q,w);
}
p=p->nextarc;
}
}
}
printf("\n");
/********** End **********/
}
int LocateVex(ALGraph G,VertexType u)
{ // 初始条件:图G存在,u和G中顶点有相同特征
// 操作结果:若G中存在顶点u,则返回该顶点在图中位置;否则返回-1
/********** Begin **********/
int i;
for(i=0;i<G.vexnum;++i)
if(strcmp(u,G.vertices[i].data)==0) return i;//"成都"之类的地名data的字符串特征比较
return -1;
/********** End **********/
}
int FirstAdjVex(ALGraph G,VertexType v)
{ // 初始条件:图G存在,v是G中某个顶点
// 操作结果:返回v的第一个邻接顶点的序号。若顶点在G中没有邻接顶点,则返回-1
/********** Begin **********/
LinkList p;
int v1;
v1=LocateVex(G,v);
p=G.vertices[v1].firstarc;
if(p)//如果指针不为空,则说明有此线段,有邻接点。
return p->data.adjvex;//输出邻接点
else
return -1;
/********** End **********/
}
int NextAdjVex(ALGraph G,VertexType v,VertexType w)
{ // 初始条件:图G存在,v是G中某个顶点,w是v的邻接顶点
// 操作结果:返回v的(相对于w的)下一个邻接顶点的序号。若w是v的最后一个邻接点,则返回-1
/********** Begin **********/
LinkList p,p1;
ElemType e;
int v1;
v1=LocateVex(G,v);
e.adjvex=LocateVex(G,w);
p=Point(G.vertices[v1].firstarc,e,equal,p1);
if(!p||!p->next)
return -1;
else
return p->next->data.adjvex;
/********** End **********/
}
void BFSTraverse(MGraph G)
{ // 图G存在,从第1个顶点起,按广度优先非递归遍历图G,并对每个顶点调用函数visit一次且仅一次
/********** Begin **********/
int v,u,w;
SqQueue Q;
for(v=0;v<G.vexnum;v++)
visited[v]=0;
InitQueue(Q);
for(v=0;v<G.vexnum;v++)
if(!visited[v]){
visited[v]=1;
visit(G.vexs[v]);
EnQueue(Q,v);
while(!QueueEmpty(Q)){
DeQueue(Q,u);//int类型的v,u都是索引
for(w=FirstAdjVex(G,G.vexs[u]);w>=0;w=NextAdjVex(G,G.vexs[u],G.vexs[w]))
if(!visited[w]){
visited[w]=1;
visit(G.vexs[w]);
EnQueue(Q,w);
}
}
}
/********** End **********/
//邻接矩阵
void DFS(MGraph G,int v)
{
// 从第v个顶点出发递归地深度优先遍历图G
/********** Begin **********/
int w;
visited[v]=1;
visit(G.vexs[v]);
for(w=FirstAdjVex(G,G.vexs[v]);w>=0;w=NextAdjVex(G,G.vexs[v],G.vexs[w]))
if(!visited[w])
DFS(G,w);
/********** End **********/
}
七.排序
排序的稳定性:若 a 在排序的过程中并不是始终领先于 b 的,则是不稳定的,否则是稳定的。只要有一组排序非稳定,则该方法就是非稳
定的。
内部排序:在计算机内存中排序;外部排序:内存放不下,还要在外存中读取。
#define MAXSIZE 20
typedef int KeyType;
typedef struct{
KeyType key;
InfoType otherinfo;
}RedType;
typedef struct{
RedType r[MAXSIZE+1];//r[0]闲置做哨兵
int length;
}SqList;
插入排序
直接插入排序
void InsertSort(SqList &L){
for(i=2;i<=L.length;i++){
if(L.r[i].key<L.r[i-1].key){
L.r[0]=L.r[i];
L.r[i]=L.r[i-1];
for(j=i-2;L.r[0].key<L.r[j].key;--j)//不用控制j的范围,因为具有哨兵值r[0],要取相等,就可以结束循环
L.r[j+1]=L.r[j];//前面比他大的都往后面移动
L.r[j+1]=L.r[0];//注意是j+1赋值
}
}
}
时间复杂度:O(n^2),空间复杂度:O(1),适用于基本有序的情况。
折半插入排序
void BInsertSort(SqList &L){
for(i=2;i<L.length;i++){
L.r[0]=L.r[i];
low=1;high=i-1;
while(low<=high){
m=(low+high)/2;
if(L.r[0].key<L.r[m].key) high=m-1;
else low=m+1;
}
for(j=i-1;j>=high+1;--j) L.r[j+1]=L.r[j];
L.r[high+1]=L.r[0];
}
}
折半插入排序是针对原本有序的情况下的插入
折半插入减少比较次数,而非移动次数。
时间复杂度:O(n^2),空间复杂度:O(1)
希尔排序
void ShellInsert(SqList &L,int dk){//dk表示增量
for(i=dk+1;i<=L.length;i++){//r[0]不算,所以在算索引值时要+1先跳过他
if(L.r[i].key<L.r[i-dk].key){
L.r[0]=L.r[i];//暂存无序值
for(j=i-dk;j>0&&L.r[0].key<L.r[j].key;j-=dk)
L.r[j+dk]=L.r[j];//比他大的往后移动,腾出位置来。
L.r[j+dk]=L.r[0];
}
}
}
void ShellSort(SQList &L,int dt[],int t){
for(k=0;k<t;k++)
ShellInert(L,dt[k]);
}
比较混乱时,先使用希尔排序,基本有序之后,再使用直接插入排序比较好。
希尔排序的时间复杂度取决于“增量”的复杂度,且最后一个增量必须为1
交换排序
冒泡排序
关键字最大的两个被安排到最后面
void BubbleSort(SqList &L){
m=L.length-1;flag=1;
while((m>0)&&(flag==1)){//flag=0时,表示这趟顺序没有被交换,不会执行下一趟排序
flag=0;//如果没有发生交换,则顺序是对的,不用再继续冒泡
for(j=1;j<=m;j++){
if(L.r[j].key>L.r[j+1].key){
flag=1;
t=L.r[j];
L.r[j]=L.r[j+1];
L.r[j+1]=t;//开始冒泡,交换记录
}
m--;//最高的范围边界往下放
}
}
}
总的关键比较次数KCN,移动次数RMN。
时间复杂度:O(n^2),看循环个数就可以了,不用老实去算。
此方法移动次数较多,初始记录无序,n较大时,不应采用
快速排序
pivotkey来记录中枢,说白了就是找到中间根树的排序方法,交换是与枢纽之间的。
int Partition(SqList &L,int low,int high){
L.r[0]=L.r[low];//一开始第一个记录作为枢纽记录
pivotkey=L.r[low].key;
while(low<high){
while(low<high&&L.r[high].key>=pivotkey) --high;
L.r[low]=r[high];
while(low<high&&L.r[low].key<=pivotkey) ++low;
L.r[high]=L.r[low];
}
L.r[low]=L.r[0];
return low;
}
void QSort(SqList &L,int low,int high){
if(low<high){
pivotloc=Partition(L,low,high);
QSort(L,low,pivotloc-1);
QSort(L,pivotloc+1,high);
}
}
时间复杂度:O(nlog2n)
当生成树比较平衡时,这个算法才简单,1,n/2,n,选取中间数与第一个交换,作为第一次的pivotkey,是比较适合的。
适用于顺序结构,很难用于链式结构,适用于初始无序,n较大的情况。
比较是按照枢纽比较,交换是按照边界交换
选择排序
简单选择排序
每次都选择最小的,交换到它应该所在的位置上。
void SelectSort(SQList &L){
for(i=1;i<L.length;i++){
k=i;//初始化指向最小位置的指针
for(j=i+1;j<L.length;j++)
if(L.[j].key<L.[k].key) k=j;//指向该次排序中最小的关键字
if(k!=i){
t=L.r[i];
L.r[i]=L.r[k];
L.r[k]=t;
}
}
}
简单选择排序,因为采用记录模式,所以移动次数较小,最少情况:3(n-1)
在n个关键字中选择最小的,需要n-1次比较,但是在n-1个关键字中选出次小的不一定定需要n-2次比较
树形选择排序
含有n个叶子结点的完全二叉树的深度为log2n(向上取整),每一个需要进行log2n次的比较,时间复杂度:O(nlog2n)
当被选过了就改为﹢无穷,在生成小根堆的时候就不会选到他。
堆排序
堆排序分为大根堆与小根堆,每个点都应该满足这种堆的关系。
调整堆就像筛选法一样,只破坏了某一边树枝的平衡,那就只修改那一边就可以了。
筛选法调整堆
void HeapAdjust(SqList &L,int s,int m){//r[s+1..m已经是堆],重新调成以s为顶的堆
rc=L.r[s];
for(j=2*s,j<=m;j=j*2){
if(j<m&&L.r[j].key<L.r[j+1].key) ++j;//较大的记录的下标
if(rc.key>=L.r[j].key) break;//达到平衡,就退出循环
L.r[s]=L.r[j];
s=j;//把较大值一层一层往上放
}
L.r[s]=rc;
}
建初堆
每一个结点为根的子树都要调整为堆,但所有>n/2(向下取整)的点1都为叶子
注意:关键就是从最后一个非叶子结点从下往上开始建立。
分为小根堆,大根堆
注意:要从底层往上建立堆。
void CreateHeap(SqList &L){
n=L.length;
for(i=n/2;i>0;i--){//这个i的初值就相当于向下取整了。
HeapAdjust(L,i,n);
}
}
堆排序
升序排列
void HeapSort(SqList &L){
CreateHeap(L);//调为大根堆
for(i=L.length;i>1;i--){
x=L.r[1];
L.r[1]=L.r[i];
L.r[i]=x;//最大值放在最后面,交换位置
HeapAdjust(L,1,i-1);//重新调为大根堆。
}
}
时间复杂度:O(nlog2n),且只能使用于顺序结构。堆排序的性能接近于最坏性能。
注意:堆排序的时候是要的最大值,但最终结果要的是从小到大。
选择填空
从未排序序列中挑选元素,并将其依次放入已排序序列(初始时为空)的一端的方法,称为选择排序。
从未排序序列中依次取出元素与已排序序列中的元素进行比较,将其放入已排序序列的正确位置上的方法,这种排序方法称为插入排序。
数据表中有10000个元素,如果仅要求求出其中最大的10个元素,则采用堆排序算法最节省时间。
注意:关键就是从最后一个非叶子结点从下往上开始建立。
对n个关键字作冒泡排序,在最好情况下,算法的时间复杂度是,若初始序列为“正序”,则只需进行一趟排序,在排序过程中进行n-l次比较,且不
移动记录,因此时间复杂度为n。
快速排序在完全无序的情况下最易发挥其长处。快速排序利用中间数,时间复杂度:O(nlog2n)
对n个不同的排序码进行冒泡排序,在元素无序的情况下比较的次数最多为:n(n-1)/2 n-1一直加到1。
希尔排序,他只是一个大体排序,不能保证每趟排序至少能将一个元素放到其最终的位置上。
八.查找
查找表是集合,关键字分为主关键字和次关键字,有修改称为动态查找表。
平均查找长度:ASL,R[0]不需要使用
顺序查找
int Search_Seq(SSTable ST,KeyType key){
for(i=ST.length;i>=1;i--){
if(ST.r[i].key==key) return i;
}
return 0;
}
int Search_Seq(SSTable ST,KeyType key){
ST.R[0].key=key;
for(i=ST.length;ST.R[i].key!=key;key--);
return i;
}//这个方法减少了一半的时间,但主要是减少的判断时间。
折半查找
又称二分查找,必须采用顺序存储结构,有序排列。
int Search_Bin(SSTable ST,KeyType key){
low=1;high=STlength;
while(low<=high){
mid=(low+high)/2;
if(key==SR.R[mid].key) return mid;
else if(key<ST.R[mid].key) high=mid-1;
else low=mid+1;
}
return 0;
}//low=high时还要在while里面比较,才能return mid,当low>high时,就结束了循环。
ASL=log2(n+1)-1
折半查找不适用于数据元素经常变动的线性表。
二叉排序树
二叉排序树或者是一颗空树,右子树不空,右子树所有的结点都大于根结点的值。
中序遍历可以得到一颗结点值递增的序列。
BSTree SearchBST(BSTree T,KeyType key){
if((!T)||key==T->data.key) return T;
else if(key<T->data.key) return SearchBST(T->rchild,key);
else return SearchBST(T->rchild,key);
}
二叉排序树不唯一,先后插入的关键字有序时,将会蜕变为单支树
插入二叉排序树
void InsertBST(BSTree &T,ElemType e){
if(!T){
S=new BSTNode;
S->data=e;
S->lchild=S->rchild=nullptr;
T=S;
}else if(e.key<T->data.key) InsertBST(T->lchild,e);
else if(e.key>T->data.key) InsertBST(T->rchild,e);
}
二叉树的创建
void CreateBST(BSTree &T){
T=nullptr;
cin>>e;
while(e.key!=ENDFLAG){
InsertBST(T,e);
cin>>e;
}
}
时间复杂度:O(nlog2n)
每次插入的结点,都成为叶子结点
二叉排序树的删除
void DeleteBST(BSTree &T,KeyType key){
p=T;
f=nullptr;
while(p){
if(p->data.key==key) break;
f=p;
if(p->data.key>key) p=p->lchild;
else p=p->rchild;
}
if(!p) return;
q=p;
if((p->lchild)&&(p->rchild)){
s=p->lchild;
while(s->rchild){
q=s;
s=s->lchild;
else q->lchild=s->lchild;
delete s;
return;
}
}
else if(!p->rchild){
p=p->lchild;
}else if(!p->lchild){
p=p->child;
}
if(!f) T=p;
else if(q==f->lchild) f->lchild=p;
else f->rchild=p;
delete q;
}
散列表
散列查找法:Hash Search
p=H(key),散列函数,散列地址
散列表:一个有限连续的地址空间,同义词
好的散列表:计算简单,地址分布均匀
数字分析法、平方取中法(ASCII作为内部编码)
折叠法:将关键字分割为几位位数相同的,最后一个可以不同。分为移位叠加和边界叠加。适用于地址位数少,且关键字位数比较多。
除留余数法:H(key)=key%p;p可以选小于key的最大质数。
处理冲突的方法
开放地址法
Hi=(H(key)+di)%m
迭代计算,直到没有冲突的位置被找到。H0作为基础
线性探测:di=1,2,3,***,m-1
二次探测:di=12,-12,2^2,来回折叠
伪随机探测
开放地址法的缺点:容易发生二次聚集,“堆积”,争夺同一个后继散列地址
链地址法
具有相同散列地址的记录放在同一个单链表中,称为同义词链表,m个散列地址有m个单链表
HT[0***m-1]存放头指针
九.数据结构-选填
- 线性表的逻辑顺序与物理顺序不会总是一致的。(T)
- 不是每种数据结构都应具备三种基本运算:插入、删除和搜索。(T)
- 线性表中的每个结点最多只有一个前驱和一个后继。(F)
- 删除二叉排序树中一个结点,再重新插入上去,一定能得到原来的二叉排序树。(F)
- 多维数组是向量的推广。(F)
- 一般树和二叉树的结点数目都可以为 0。(T)
- 直接选择排序是一种不稳定的排序方法。(F)
- 在只有度为0 和度为 k 的结点的 k 又树中,设度为0 的结点有 n0个,度为k 的结点有n1个,则有n0=n1+1。(F)