数据结构期末复习

第一章 绪论

算法和算法分析

主要要求会计算时间复杂度

// 对数阶
 
int result=1;
 
while(result<n){
 
    result=result*2; //时间复杂度为O(1)
 
}

此代码的时间复杂度是O(logn)

面对多个复杂组合,选取时间复杂度最高的语句作为最终结果

// 多个复杂度组合
 
for(int i=0;i<n;i++){   
   for(int j=0;j<n;i++){ 
       System.out.println(result[i][j]);  //执行一次 
   } 
}
 
for(int i=0;i<n;i++){  
   System.out.println(result[i]);  //执行一次 
}

时间复杂度为O(n²)

遇到递归:

//求阶乘
long long Factorial(int N) {
	return N < 2 ? N : N * Factorial(N - 1) ;
}

时间复杂度为O(n)
如果遇到更为复杂的递归,如:

//斐波那契函数
long long Fibonacci(int N) {
	return N < 2 ? N : Fibonacci(N - 1) + Fibonacci(N - 2);
}

时间复杂度:O(\(n^2\))
image

第二章 线性表

线性表的类型定义

线性表基本操作

InitList(&L);
//操作结果,构造一个空的线性表L
DestroyList(&L);
//初始条件:线性表L已经存在
//操作结果:销毁线性表L
ClearList(&L);
//操作结果:将表置为空表
ListEmpty(L);
//判断表是否为空 如果为空,返回TRUE,否则返回FALSE
ListLenght(L);
//返回L中的数据元素个数
GetElem(L,i,&e);
//用e返回L中第i个位置的元素值
LocateElem(L,e,compare());
//返回L中第一个与e满足关系compare的数据元素的位序,如果这样的数据元素不存在,返回值为0
ListDelete(&L,i,&e);
//删除L的第i个数据元素,并用e返回其值,L的长度-1

算法2.1
已知线性表LA和LB中的数据元素按照值非递减有序排列,现要求将LA和LB归并为一个新的线性表LC,且LC中的数据元素仍按照值非递减有序排列

void MergeList(List LA, List LB, List &LC) {
    InitList(LC);
	i = j = 1;
	k = 0;
	while((i <= LA.len) && (j <= LB.len)){//判断LA 和LB都非空
		GetElem(LA,LA.list[i],ai);
		GetElem(LB,LA.list[j],bj);
		if(LA.list[i] >= LA.list[j]) {
			Insert(LB.list[j],k++,&LC);//将LB.list[j]插入LC的k位置中,并将k++
			j++;
		}
		else {
			Insert(LA.list[i],k++,&LC);
			i++;
		}
	}
	while(i <= LA.len) {
		Insert(LA.list[i],k++,&LC);
		i++;
	}
	while(j <= LA.len) {
		Insert(LB.list[j],k++,&LC);
		j++;
	}
}

(自己写的代码,和书上有些微出入)

线性表的顺序表示和实现

通常采用顺序存储结构,数组。
顺序结构的插入删除,最大的难点在于元素的移动,因为元素在物理上顺序存放,导致改变其中一个值,就会影响到其他值的位置。

顺序表的优点:

  • 地址连续,方便排序,很多排序算法都是基于数组地址的连续。因为地址连续,所以顺序表支持下标的随机访问,可以在任意合法下标内随机访问数据。

顺序表的缺点:

  • 因为顺序表地址连续,在进行头插头删和pos位置插入数据时都要挪动数据。头插头删时间复杂度是O(N),pos位置删除插入要挪动N - (pos+1)次,但pos也可能是0的位置,也就是头删,所以pos位置删除插入的时间复杂度也是O(N)。
  • 顺序表都会有空间浪费,无论是静态顺序表还是动态顺序表都无法避免空间浪费的问题,只不过动态顺序表比静态顺序表要更加灵活一点。

线性表的链式表示和实现

即线性链表
链表特点

  • 链表是用一组任意的存储单元来存放线性表的结点,这组存储单元既可以是连续的,也可以是不连续的,甚至是零散分布在内存中的任何位置上
  • 因此,链表中结点的逻辑次序和物理次序不一定相同。
  • 因为链表除了存放数据元素之外,还要存放指针,因此链表的存储密度不及顺序存储。

单链表的基本操作

InitLink()        // 初始化
GetElem(L, i, e)  // 取第i个数据元素
ListInsert(&L, i, e)  // 插入数据元素
ListDelete(&L, i, e)  // 删除数据元素
ClearList(&L)     // 重置线性表为空表
CreateList(&L, n) // 生成含 n 个数据元素的链表
  • InitLink()
LinkList InitList( )
{

   LinkList L=(LinkList) malloc (sizeof(LNode));
      //建立头结点

    L->next=NULL;

    return L;//建立空的单链表L

}

链表插入

  • ListInsert()
void ListInsert(LinkList L, int i, int e) {
	p = L;//指针p指向头节点L
	while (p && j < i - 1) {
		p = p.next;
		j++;
	}//p不为空且j < i - 1
//寻找第i - 1 个节点
	if (!p || j > i - 1)
		return;
	else {
		s = (LinkList)malloc(sizeof(LNode));//生成新节点
		s.data = e;
		s.next = p.next;
		p.next = s;
		return;
	}
}
//算法的时间复杂度为:O(ListLength(L))

链表删除

//略 大体思路和插入相同 找到第i - 1 个节点所在的位置
//算法复杂度也和插入相同

链表加入头节点的用处:

  • 由于头结点的位置被存放在头结点的指针域中,所以在链表的第一个位置上的操作就和在表的其他位置上的操作一致,无须进行特殊处理。
  • 无论链表是否为空,其头指针是指向头结点的非空指针(空表中头结点的指针域空),因此空表和非空表的处理也就一致了。

从线性表得到单链表
操作步骤:

  • 建立一个空表
  • 输入数据元素An,建立节点并插入
  • 输入数据元素An-1,建立节点并插入
  • 依此类推,直至输入A1为止

两种方法: 头插法 尾插法

单链表操作的时间复杂度

操作 时间复杂度
GetElem(L, i, e) O(n)
ListInsert(&L, i, e) O(n)
ListDelete(&L, i, e) O(n)
ClearList(&L) O(n)
CreateList(&L, n) O(n)

用上述定义的单链表实现线性表的操作时,存在的问题:

  1. 单链表的表长是一个隐含的值
  2. 在单链表的最后一个元素之后插入元素的时候,需要遍历整个链表
  3. 在链表中,元素的位序概念被淡化,节点的位置概念加强

改进链表的设置:

  1. 增加“表长”、“表尾指针”、“当前位置的指针”三个数据域
  2. 将基本操作中的“位序i”改变为“指针p”

循环链表

最后一个节点的指针域的指针又指回第一个结点的链表
和单链表的差别仅仅在于:判别链表中最后一个结点的条件不再是后继是否为空,而是后继是否为头节点

LinkList Connect(LinkList A, LinkList B) {
	//假设A,B为非空循环链表的尾指针
	(1)LinkList P = A->next;
	(2)A->next = B->next->next;
	(3)free(B->next);
	(4)B->next = p;
	(5)return B;
}

每条语句的含义:

  1. 把P指向A的头节点
  2. 把A的尾指针的next指向B的头节点的next
  3. free B 的头节点
  4. B的尾指针的next指向A的头节点
  5. 返回B 的尾指针

此算法结束后,B 成为新的循环链表的尾指针。

程序阅读

LinkList Demo(LinkList L) {//L是无头节点的单链表
	(1)LinkNode *Q, *P;
	(2)if(L && L->next) 
		{
		(3)Q = L; L = L->next; P = L;
		while(P->next)
			(4)P = P->next;
		(5)P->next = Q; Q->next = NULL;
		}
	return L;
}

每个语句的含义

  1. 创建两个顶点指针,指向每一个链表的数据域
  2. 判断 L 不为空,且 L 的下一个元素也不为空
  3. L 的地址赋给Q,L指向下一个元素,下一个元素的地址赋给P
  4. while语句,当P的下一个不为空的时候,P指向下一个元素(即找到最后一个元素)
  5. 将尾结点的指针指向头结点,头结点的指针指向空

本算法将头结点变成了新的尾结点

双向链表

基本结构

typedef struct  DuLNode {

    ElemType         data;   // 数据域

    struct DuLNode   *prior;  
                               // 指向前驱的指针域

    struct DuLNode  *next;  
                               // 指向后继的指针域

} DuLNode, *DuLinkList;

结构图示
双向循环链表
插入操作

Status ListInsert_ DuL (DuLinkList &L, int i, ElemType e) {
          //i的合法值为1≤i≤表长+1. 
          if (!(p=GetElemP_ DuL(L,i))) //大于表长加1,p=NULL;
          return ERROR;                //等于表长加1时,指向头结点;
                                                     
          if (!(s=(DuLinkList) malloc(sizeof (DuLNode))) 
          return ERROR;

          s->data=e;
          s->prior=p->prior; p->prior->next=s;
          s->next=p;            p->prior=s;
          return OK;

}// ListInsert_ DuL 

删除操作

Status ListDelete_ DuL (DuLinkList &L, int i, ElemType &e) {
     //i的合法值为1≤i≤表长
          if (!(p=GetElemP_ DuL(L,i))) 
          return ERROR;  //p=NULL,即第i个元素不存在 

          e=p->data;
          p->prior->next=p->next;
          p->next->prior=p->prior;

          free (p);         return OK;

}// ListDelete_ DuL 

几种主要链表的比较
image

链表的边界条件
几个特殊点的处理:

  • 头指针处理
  • 非循环链表尾结点的指针域保持为NULL
  • 循环链表尾结点的指针回指头结点

链表处理

  • 空链表的特殊处理
  • 插入或者删除结点时指针钩链的顺序
  • 指针移动的正确性
    • 插入
    • 查找或者遍历

线性表实现方法的比较

顺序表的主要优点

  • 没有使用指针,不用花费额外的开销
  • 线性表元素的读访问非常简洁便利

链表的主要优点

  • 无需事先了解线性表的长度
  • 允许线性表的长度动态变化
  • 能够适应经常插入删除内部元素的情况

顺序表是存储静态数据的不二选择
链表是存储动态变化数据的良方

PPT算法实例

  • 实现集合运算(A-B)U(B-A)

算法实现思想:
假设由终端输入集合元素,先建立表示集合A的静态链表S,而后在输入集合B的元素的同时查找表S,若存在和B相同的元素,则从S表中删除之,否则将此元素插入S表。

  • 一元多项式的表示和相加

需要分系数和指数两个数据域

  • 实现顺序表的就地逆置

将顺序表中的对称元素互换即可
基本操作 数据交换

  • 实现单链表的就地逆置

单链表从表头进行插入得到的元素顺序与输入顺序相反。因此,以原链表作为输入,头结点不变,从第一个元素开始到最后一个元素,逐个插入到头结点的后面。

第三章 栈和队列

栈(Stack)是限定仅在表的一端进行插入或删除操作的线性表。通常称插入删除的一端为栈顶(top),另一端称为栈底(bottom)
image

基本操作函数

InitStack(&S);
//操作结果:构造一个空栈S。
DestrtyStack(&S);
//初始条件:栈S已经存在
//操作结果:销毁栈S
ClearStack(&S);
//初始条件:栈 S 已存在。
//操作结果:将 S 清为空栈。
StackEmpty(S);
//初始条件:栈 S 已存在。
//操作结果:若栈 S 为空栈,则返TRUE,否则FLASE
StackLength(S);
//初始条件:栈 S 已存在。
//操作结果:返回 S 的元素个数,即栈的长度。
GetTop(S, &e);
//初始条件:栈 S 已存在且非空。
//操作结果:用 e 返回 S 的栈顶元素。
Push(&S, e);
//初始条件:栈 S 已存在。
//操作结果:插入元素 e 为新的栈顶元素。
Pop(&S, &e);
//初始条件:栈 S 已存在且非空。
//操作结果:删除 S 的栈顶元素,并用 e 返回其值
StackTraverse (S, visit());
//初始条件:栈 S 已存在。
//操作结果:从栈底到栈顶依次对S 的每个数据元素调用函数visit()。一旦visit()失败,则操作失败。

一个小例题
如果三个字符按照ABC顺序压入堆栈

  • ABC的所有排列都可能是出栈的顺序吗
  • 可以产生CAB这样的序列吗
  • 总共又集中排列的可能性呢

顺序栈的类型定义

#define  StackSize  100  
//假定预分配的栈空间最多为100个元素
typedef char DataType;
//假定栈元素的数据类型为字符  
Typedef struct{
        DataType  data[StackSize];
        int  top;
}SeqStack;

顺序栈示意
image

用一个数组实现两个堆栈
可以从两头向中间生长,当两个栈的栈顶指针相遇时,表示两个栈都满了
这样可以极大程度上的利用栈内的空间

栈的链式存储实现
image
一般是采用头插法,保留一个头结点,所有的元素都插在头结点之后
头结点就是栈顶

顺序栈和链式栈的比较

  • 时间效率
    • 所有操作都只需常数时间
    • 顺序栈和链式栈在时间效率上难分伯仲
  • 空间效率
    • 顺序栈须说明一个固定的长度
    • 链式栈的长度可变,但增加结构性开销

实际应用中,顺序栈比链式栈用得更广泛些

  • 顺序栈容易根据栈顶位置,进行相对位移,快速定位并读取栈的内部元素
  • 顺序栈读取内部元素的时间为O(1),而链式栈则需要沿着指针链游走,显然慢些,读取第k个元素需要时间为O(k)

一般来说,栈不允许“读取内部元素”,只能在栈顶操作

栈的应用举例

  1. 数制转换

例子:
十进制转八进制
image
关键:出入栈操作

  1. 括号匹配的检验

算法设计思想:

  1. 凡出现左括弧,则进栈
  2. 凡出现右括弧,首先检查栈是否空,若栈空,表面该右括弧多余,否则,和栈顶元素比较,如果相匹配,则左括弧出栈,否则表明不匹配
  3. 表达式检验结束时,若栈空,则表明表达式中匹配正确,否则表明左括弧有余
  1. 行编辑程序问题

问题描述 一个简单的行编辑程序的功能是:接受用户从终端输入的程序或数据,并存入用户的数据区。
要求 在用户输入一行的过程中,允许用户输入出差错,并在发现有误时可以及时更正。
合理做法 设立一个输入缓冲区,用以接受用户输入的一行字符,然后逐行存入用户数据区,并假设“#”为退格符,“@”为退行符。
假设从终端接受了这样两行字符:
whli##ilr#e(s#s)
outcha@putchar(
s=#++);
则实际有效的时下列两行:
while (s)
putchar(
s++);
具体实现:

Void LineEdit ( )   {  //利用字符栈S,从终端接受 //一行并传送至调用过程的数据区。
InitStack (S);       //构造空栈S
ch = getchar ();    //从终端接收第一个字符
while (ch != EOF) { //EOF为全文结束符
	while (ch != EOF && ch != '\n') {
		  switch (ch) {
			   case ‘#’ : Pop(S, c); break;     //仅当栈非空时退栈
			   case '@': ClearStack(S); break;   // 重置S为空栈
			   default : Push(S, ch);  break;
							   //有效字符进栈,未考虑栈满情形
		  }//end switch
		  ch = getchar( );  // 从终端接收下一个字符
      }//end while
	  ClearStack(S);      // 重置S为空栈
	  if (ch != EOF)  ch = getchar();

}//end while
DestroyStack (S);
}//LineEdit 

  1. 表达式求值
    了解表达式的三种表示方法
  • 前缀表示法
  • 中缀表示法
  • 后缀表示法

例如:
Exp = \(a\times b + (c\times d/e)\times f\)
前缀式
\(+\times ab\times - c/def\)
中缀式
\(a\times b + c - d/e \times f\)
后缀式
\(ab\times cde/-f\times +\)

  • 前缀式运算规则:紧靠在一起的两个操作数和距离他们最近的一个运算符构成一个最小表达式
  • 中缀式运算规则:略
  • 后缀式运算规则:运算符在式中出现的顺序恰好为表达式的运算顺序,每个运算符和在他之前出现,且紧靠它的两个操作数构成一个最小表达式

如何从后缀式求值
先找运算符,再找操作数
例如:
image
后缀表达式求值过程:

  1. 设立操作数或运算结果栈 OPND;
  2. 设表达式的结束符为“#”,予设操作数栈为空栈;
  3. 依次读入表达式中每个字符,若是操作数则进OPND栈,若是运算符则退出栈顶两个元素进行计算,直至整个表达式求值完毕(即当前读入的字符为‘#’)。

代码表示:

OperandType EvaluateExpression()  {
    //后缀表达式求值的算法。设OPND为运算数栈
  //OP为运算符集合。
   initStack (OPND); c=getchar();
    while (c!=‘#’) {
       if (!In (c, OP)) { Push (OPND, c); c= getchar();}
                                                 //不是运算符则进栈
       else  {
			Pop (OPND, b);
			Pop (OPND, a);
			Push (OPND, Operate (a, c, b));
	   }
            return GetTop(OPND);
           }//EvaluateExpression

从原表达式求得后缀表达式

每个运算符的运算次序要由它之后的一个运算符来定,在后缀式中,优先数高的运算符领先于优先数低的运算符。

从原表达式求得后缀式的规律为:

  1. 设立操作符栈;
  2. 设表达式的结束符为“#”,予设运算符栈的栈底为“#”;
  3. 若当前字符是操作数,则直接发送给后缀式。
  4. 若当前运算符的优先数高于栈顶运算符,则进栈;
  5. 否则,退出栈顶运算符发送给后缀式;
  6. “(” 对它之前后的运算符起隔离作用,“)”可视为自相应左括弧开始的表达式的结束符。

算符优先算法求表达式值过程为:

  1. 设立操作符栈OPTR,操作数或运算结果栈OPND;
  2. 设表达式的结束符为“#”,予设运算符栈的栈底为“#”,操作数栈为空栈;
  3. 依次读入表达式中每个字符,若是操作数则进OPND栈,若是运算符则和OPTR栈的栈顶运算符比较优先权后作相应操作,直至整个表达式求值完毕(即OPTR栈的栈顶元素和当前读入的字符均为‘#’)。

栈与递归的实现

一个对象如果部分地由它自身来定义(或描述),则称其为递归。
递归调用的执行情况:
image

典型例题:

汉诺塔

image
算法设计:
当n=1时 编号为1的圆盘从塔座X直接移至塔座Z上即可;
当n>1时

  1. 将压在编号为n 的圆盘之上的n-1个圆盘从
    塔座X(依照上述法则)移至塔座Y上;
  2. 将编号为n 的圆盘从塔座X移至塔座Z上;
  3. 再将塔座Y上的n-1个圆盘(依照上述法则)移至塔座Z上;

其中,将n-1个圆盘从一个塔座移至另一塔座的问题是一个和原问题相同的问题,只是问题的规模小1

void hanoi (int n, char x, char y, char z) {
       // 将塔座x上按直径由小到大且至上而下编号为1至n
   // 的n个圆盘按规则搬到塔座z上,y可用作辅助塔座。
   //搬动操作move(x, n, z)可定义为(c是初值为0的全局
       //变量,对搬动计数):printf(“%i.Move disk %i from
       // %c  to %c\n”, ++c, n, x, z);
{
    if (n==1)
       move(x, 1, z);              // 将编号为1的圆盘从x移到z
    else {
    	hanoi(n-1, x, z, y);      // 将x上编号为1至n-1的圆盘移到y, z作辅助塔
    	move(x, n, z);              // 将编号为n的圆盘从x移到z
       	hanoi(n-1, y, x, z);      // 将y上编号为1至n-1的
        }
}

队列

  • 队列(queue)是限定只能在表的一端进行插入,在表的另一端进行删除的线性表。
  • 容许插入的一端称做队尾(rear)
  • 容许删除的一端称为队头(front)
  • 队列的特点:先进先出(Fisrt In First Out) FIFO
    image

基本操作

InitQueue(&Q)
//操作结果:构造一个空队列Q。
DestroyQueue(&Q)
//初始条件:队列Q已存在。
//操作结果:队列Q被销毁,不再存在。
QueueEmpty(Q)
//初始条件:队列Q已存在。
//操作结果:若Q为空队列,则返回TRUE,否则返回FALSE。
QueueLength(Q)
//初始条件:队列Q已存在。
//操作结果:返回Q的元素个数,即队列的长度。
GetHead(Q, &e)
//初始条件:Q为非空队列。
//操作结果:用e返回Q的队头元素。
ClearQueue(&Q)
//初始条件:队列Q已存在。
//操作结果:将Q清为空队列。
EnQueue(&Q, e)
//初始条件:队列Q已存在。
//操作结果:插入元素e为Q 的新的队尾元素。
DeQueue(&Q, &e)
//初始条件:Q为非空队列。
//操作结果:删除Q的队头元素,并用e返回其值。

队列两种表示形式:

  • 链式
  • 顺序

由于顺序队列会导致空间的浪费
(如图)
image
故而有循环队列
image
image
代码:

 Status InitQueue (SqQueue &Q) {
   // 构造一个空队列Q
   Q.base = (ElemType *) malloc 
        (MAXQSIZE *sizeof ElemType));
    if (!Q.base) exit (OVERFLOW);
               //存储分配失败
     Q.front = Q.rear = 0;
     return OK;
 }
Status EnQueue (SqQueue &Q, ElemType e) {   
     // 插入元素e为Q的新的队尾元素
    if ((Q.rear+1) % MAXQSIZE == Q.front) //关键!
        return ERROR;     //队列满
    Q.base[Q.rear] = e;
    Q.rear = (Q.rear+1) % MAXQSIZE;//此处关键,想要循环,需要在此处进行取余操作
    return OK;
}
 Status DeQueue (SqQueue &Q, ElemType &e) { 
    // 若队列不空,则删除Q的队头元素,
   // 用e返回其值,并返回OK;  否则返回ERROR
    if (Q.front == Q.rear)  return ERROR;
    e = Q.base[Q.front];
    Q.front = (Q.front+1) % MAXQSIZE;
    return OK;
}

第四章 串

串是有限长的字符序列,由一对单引号相括,如: 'a string'

基本操作函数:

 StrAssign (&T, chars)
 //初始条件:chars 是字符串常量。
 //操作结果:把 chars 赋为 T 的值。

 DestroyString(&S)
 //初始条件:串S存在
 //操作结果:串S被销毁

 StrCopy (&T, S)
 //初始条件:串S存在
 //操作结果:由串S赋值得串T

 StrLength(S)
 //初始条件:串S存在
 //操作结果:返回S的元素个数,成为串的长度

 StrCompare (S, T)
 //比较两个串,可以看成S - T
 //如果S > T,返回值 > 0
 //如果S = T,返回值 = 0
 //如果S < T,返回值 < 0

 Concat (&T, S1, S2)
 //用T返回由S1,S2连接而成的新串

 SubString (&Sub, S, pos, len)
 //用sub返回串S的第pos个字符起长度为len的子串

 ClearString (&S)
 //将串清为空

 Index (S, T, pos)
 //如果主串S中存在和串T值相同的字串,则返回它在主串S中第pos个字符之后第一次出现的位置,否则函数为0
//假设 S = 'abcaabcaaabc',  T = 'bca'
//Index(S, T, 1) = 2;
//Index(S, T, 3) = 6;
//Index(S, T, 8) = 0;
//注意:子串在主串中的位置指的是字串中的第一个字符在主串中的位序!!!位序!!!

 Replace (&S, T, V)
 //用V替换主串S中出现的所有与T相等的不重叠的子串

 StrInsert (&S, pos, T)
 //在串S的第pos个字符之前插入串T
 StrDelete (&S, pos, len)
 //从串S中删除第pos个字符起,长度为len的子串。

 StrEmpty (S)
 //若 S 为空串,则返回TRUE,否则返回 FALSE

串的表示和实现

定长顺序存储表示
堆分配存储表示
串的块链存储表示

串的存储密度:
存储密度 = \(\frac {串值所占的存储位} {实际分配的存储位}\)

模式匹配原始算法:
image
效率较低
设目标长度为N 模式长度为M

  • 总比较次数M(N - M + 1)
  • 时间复杂度:O(MN)

KMP算法
image
image
KMP算法的关键:模式本身

KMP算法发现每个字符对应的该 k 值仅仅依赖于模式T本身,而与目标对象S无关
image
因此,next[j]可以预先计算
(next[j]表示在第j位失配后,j应该挪到模式串的第几位)
image

计算next数组练习题

已知字符串s为“abaabaabacacaabaabcc”,模式串t为“abaabc”。采用KMP算法进行匹配,第一次出现“失配”( s[i]!=t[j] )时,i=j=5,则下次开始匹配时,i和j的值分别是( C )。
A. i=1,j=0 B. i=5,j=0 C. i=5,j=2 D. i=6,j=2

数组和广义表

数组的顺序表示和实现

类型特点:

  1. 只有引用型操作,没有加工型操作
  2. 数组是多维的结构,而存储空间是一个一维的结构

两种顺序映像的方式:

  1. 以行序为主序(低下标优先)
  2. 以列序为主序(高下标优先)

例题

image

  1. 100
  2. (1 * 3 * 5 * 8 + 1 * 5 * 8 + 1 * 8 + 1)* 4 + 100

设有一个12×12的对称矩阵M,将其上三角部分的元素m(1≤i≤j≤12)按行优先存入C语言的一维数组N中,元素m(6,6)在N中的下标是( )【2018年全国试题3(2分)】
A.50 B.51 C.55 D.66
解析:元素m位与第6行第6列,M矩阵为对称矩阵,则前五行元素个数为12+11+10+9+8=50,则m元素相当于是第51个元素,数组N下标从0开始,则二维数组中m映射到一维数组中的下标是50,此题选择A。

按行优先存储的四维数组A=array[1..10,1..5,1..7,1..8],设每个数据元素占2个存储单元,基地址为10,则A[4,5,6,7]的存储位置为( )【吉林大学2017 一、1(2分)】
A.2110 B.2230 C.2120 D.2220
解析image
故而选B

矩阵的压缩存储

稀疏矩阵定义
假设m行n列的矩阵含有t个非零元素,则称 \(\delta = \frac{t}{m\times n}\) 为稀疏因子
通常认为$\delta \leq 0.05的矩阵为稀疏矩阵 $
以二维数组表示高阶的稀疏矩阵时产生的问题:

  1. 零值元素占了很大空间;
  2. 计算中进行了很多和零值的运算,遇除法,还需判别除数是否为零

有两类稀疏矩阵

  1. 特殊矩阵
    非零元在矩阵中的分布有一定规则

    例如: 三角矩阵, 对角矩阵

  2. 随机稀疏矩阵
    非零元在矩阵中随机出现

随机稀疏矩阵的压缩存储方法:

  • 三元组顺序表
  • 行逻辑链接的顺序表
  • 十字链表

三元组顺序表
结构:

#define  MAXSIZE  12500
 typedef struct {
     int  i, j;      //该非零元的行下标和列下标
     ElemType  e;    // 该非零元的值
 } Triple;  // 三元组类型
typedef union {
     Triple  data[MAXSIZE + 1]; 
      int     mu, nu, tu;
	  //这三个整形数值时为了存储原矩阵的行列个数以及非零元素的个数的,方便之后还原原矩阵
} TSMatrix;  // 稀疏矩阵类型

由于压缩过后的三元组无法任意存取数据,为了更方便的随意存取数据,使用带辅助向量的三元组表示
增加两个辅助向量
NUM(i)表示每行非零元素的个数
POS(i)记录稀疏矩阵中每行第一个非零元素在三元组中的行号
三元组的转置
算法简述:
建立一个辅助向量,记录每一行有几个非零元素,第一个非零元素的位置是在三元组的什么位置上,这样就可以只扫描一遍三元组即可得到最终结果!
image

广义表

广义表是一种非线性的数据结构,它的表元素可以是原子或者广义表的一种线性表的扩展结构。

  • 广义表的长度:为表中最上层元素的个数
  • 广义表的深度:为表中括号的最大层数
  • 表头和表尾:当广义表非空时,第一个元素为广义表的表头,其余元素组成的表是广义表的表尾
    image
    广义表中的元素既可以是原子类型,也可以是列表,当每个元素都为原子且类型相同时,就是线性表
    image

第六章 树和二叉树

树的基本术语

  • 结点 数据元素 + 若干指向子树的分支
  • 结点的度 结点拥有的子树的数目
  • 树的度 树中所有结点的度的最大值
  • 叶子结点 度为零的结点
  • 分支结点 度不为0的结点
  • 树的深度(高度) 树中叶子节点所在的最大层次
    image

二叉树

二叉树的性质

  1. 在二叉树的第 i 层上至多有 \(2^{i-1}\) 个结点
  2. 深度为 k 的二叉树上至多含\(2^k - 1\) 个结点
  3. 对任何一棵二叉树,若它含有 \(n_0\) 个叶子结点,\(n_2\) 个度为2的结点,则必存在关系式:\(n_0 = n_2 + 1\)

证明:
设 二叉树上结点总数为n,则 n = n0 + n1 + n2,
其中n1为度为1的结点数。
又 二叉树上分支总数 b = n1+2n2
而 b = n-1 = n0 + n1 + n2 - 1
由此, n0 = n2 + 1 。

**两类特殊的二叉树**
- 满二叉树
指的是深度为k且含有2k-1个结点的二叉树
- 完全二叉树
树中所含的 n 个结点和满二叉树中编号为 1 至 n 的结点一一对应。
  1. 具有 n 个结点的完全二叉树的深度为\([log_2n] + 1\)
  2. 若对含 n 个结点的完全二叉树从上到下且从左至右进行 1 至 n 的编号,则对完全二叉树中任意一个编号为 i 的结点:
    (1) 若 i=1,则该结点是二叉树的根,无双亲
    否则,编号为 [i/2] 的结点为其双亲结点;
    (2) 若 2i>n,则该结点无左孩子,
    否则,编号为 2i 的结点为其左孩子结点;
    (3) 若 2i+1>n,则该结点无右孩子结点
    否则,编号为2i+1 的结点为其右孩子结点。

二叉树的存储结构

重点:二叉树的链式存储表示

  1. 二叉链表
  2. 线索链表
  3. 先中后序遍历递归算法
  4. 中序遍历非递归算法

中序遍历非递归需要借助栈
一直压栈,直到左子树为空
弹出,cout,进入右子树,压栈,判断左子树是否为空,不为空,继续压栈。重复上述操作。直到栈空且无右子树

void Midorder(struct BiTNode *t)      //t为根指针 
{ struct BiTNode *st[maxleng];//定义指针栈
  int top=0;                  //置空栈
  do{            
    while(t)               //根指针t表示的为非空二叉树 
    { if (top==maxleng) exit(OVERFLOW);//栈已满,退出
      st[top++]=t;             //根指针进栈
      t=t->lchild;             //t移向左子树
     }   //循环结束表示以栈顶元素的指向为根结点的二叉树
         //的左子树遍历结束
    if (top)                    //为非空栈   
    { t=st[--top];             //弹出根指针
      printf("%c",t->data);    //访问根结点
      t=t->rchild;             //遍历右子树
     }
   } while(top||t); //父结点未访问,或右子树未遍历
 }

PPT上中序遍历非递归算法:

Status InOrderTraverse( BiTreee T, Status 
                       ( *Visit)(TElemType e) ) {
    InitStack(S);   Push(S,T);    //根指针进栈
    while (! StackEmpty(S)) {
      while (GetTop(S,p) && p) Push(S,p->lchild);
           //向左走到尽头
        Pop(S,p);        //空指针退栈
        if (!StackEmpty(S)) {   //访问结点,向右一步
        Pop(S,p);  if( !Visit(p->data)) return ERROR;
        Push(S,p->rchild);
        }//if
     }//While
                     return OK;
                 }// InOrderTraverse

线索二叉树

遍历二叉树后,可以得到一个结点的线性序列,指向该线性序列中的“前驱”和“后继” 的指针,称作“线索”
image
在二叉链表的结点中增加两个标志域LTag 和RTag并作如下规定:

  • 若该结点的左子树不空,则Lchild域的指针指向其左子树,且左标志域 LTag的值为 0“指针 Link”;
    否则,Lchild域的指针指向其“前驱”,且左标志LTag的值为 1“线索 Thread” 。
  • 若该结点的右子树不空,则rchild域的指针指向其右子树,且右标志域RTag的值为 0 “指针 Link”;
    否则,rchild域的指针指向其“后继”,且右标志RTag的值为 1 “线索 Thread”。

如此定义的二叉树的存储结构称作线索链表。以某种次序遍历使其变为线索二叉树的过程叫做线索化。
线索二叉树遍历算法(中序遍历)

void InOrderTraverse_Thr(BiThrTree T, 
                                  void (*Visit)(TElemType e)) {
  p = T->lchild;       // p指向根结点
  while (p != T) {     // 空树或遍历结束时,p==T
     while (p->LTag==Link)  p = p->lchild;  
     if (!Visit(p->data))  return error  //访问其左子树为空的结点
     while (p->RTag==Thread && p->rchild!=T) {
         p = p->rchild;  Visit(p->data);      // 访问后继结点
     }
     p = p->rchild;          // p进至其右子树根
  }
} // InOrderTraverse_Thr

CSDN算法

template<class T>
void Thread_Binary_tree<T>::_InOrder_Op(BT_Thread_Node<T>* &Tree)
{
	if (Tree == NULL)
		return;
	
	BT_Thread_Node<T>* Cur_Node = Tree;
 
	while (Cur_Node)			//当前节点不能为空
	{
		while (Cur_Node->Ltag == Link)		//节点有左树时,寻找最左端的树
		{
			Cur_Node = Cur_Node->Left_Child;
		}
		cout << Cur_Node->Data << " ";
 
		while (Cur_Node&&Cur_Node->Rtag == Thread)	//节点非空并且右树是线索树时查找最前的一个线索树节点
		{
			Cur_Node = Cur_Node->Right_Child;
			cout << Cur_Node->Data << " ";
		}
 
 
		Cur_Node = Cur_Node->Right_Child;		//右树不是线索树,查找该节点的右孩子节点
	}
	cout << endl;
}

核心思想:
先一直找到最左边的那个,然后输出,然后向右找,有线索就先找线索,直接输出,线索断了就找右孩子,继续循环
循环条件:指针不为空

树和森林

森林和二叉树的转换
思想:
兄弟变为右孩子,第一个最左边的孩子变为左孩子
森林中的所有根节点可以看成是兄弟结点
image
image
树和森林的遍历
树的遍历有三条搜索路径

  • 先根(次序)遍历:
  • 后根(次序)遍历:
  • 按层次遍历:
    image

森林的遍历:

  • 先序遍历森林
  • 中序遍历森林

森林的中序遍历顺序与该森林对应的二叉树的中序遍历顺序相同。
直接对森林操作的时候有点像后序遍历

哈夫曼编码

从叶子到根逆序求HAFFMAN树
大体步骤

  • 找到两个权值最小的,合二为一,变成一个新的树
  • 重复上述过程
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
 
typedef struct{
int weight;                //权值
int parent,lchild,rchild;  //双亲以及左右孩子
}HTNode,*HuffmanTree;  //动态分配数组存储霍夫曼树
 
typedef char* *HuffmanCode;//动态分配数组存储霍夫曼编码
 
//选择出最小的两个结点
void select(HuffmanTree &HT,int k,int &s1,int &s2){
     int i;
     i=1;
     while(i<=k && HT[i].parent!=0)i++;
     //s1指向最小结点
     s1=i;    
     for(i=1;i<=k;i++)
     {       
         if(HT[i].parent==0&&HT[i].weight<HT[s1].weight)s1=i;
     }
     //s2指向次小结点
     for(i=1;i<=k;i++)
     {
         if(HT[i].parent==0&&i!=s1)
			 break;
     }
     s2=i;  
     for(i=1;i<=k;i++)
     {
     if(HT[i].parent==0&&i!=s1&&HT[i].weight<HT[s2].weight)
		 s2=i;
     }
 
}
 
//构建霍夫曼树
void HuffmanCoding(HuffmanTree &HT,HuffmanCode &HC,int *w,int n){
 int m,c,f,s1,s2,i,start;
     char *cd;
     if(n<=1)return;
     m=2*n-1;        //n个叶子结点,n-1个非叶子结点
	 HT=(HuffmanTree)malloc((m+1)*sizeof(HTNode));  //预分配m+1个单元,0号单元未用
     HuffmanTree p=HT+1; 
     w++;   //w从1号开始
     for(i=1;i<=n;i++,p++,w++)	//对n个叶子结点赋初值
     {
         p->weight=*w;
         p->parent=p->rchild=p->lchild=0;
     }
     for(;i<=m;++i,++p)			//对另外n-1个叶子结点赋初值
     {
         p->weight=p->parent=p->rchild=p->lchild=0;
     }
 
     for(i=n+1;i<=m;i++)
     {
		 select(HT,i-1,s1,s2);   //选出当前权值最小的两个结点s1和s2
         HT[s1].parent=i;		 //修改结点s1的双亲值
         HT[s2].parent=i;		 //修改结点s2的双亲值
         HT[i].lchild=s1;		 //修改双亲结点的左孩子结点
         HT[i].rchild=s2;		 //修改双亲结点的右孩子结点
         HT[i].weight=HT[s1].weight+HT[s2].weight;  //修改双亲结点的权值
     }
   //从叶子到根逆向求每个字符的霍夫曼编码
     HC=(HuffmanCode)malloc((n+1)*sizeof(char*)); //分配n个字符编码的头指针变量
     cd=(char*)malloc(n*sizeof(char));   //分配求编码的工作空间
     cd[n-1]='\0';//编码结束符
     for(i=1;i<=n;i++)   //逐个字符求霍夫曼编码
     {   
         start=n-1;            //编码结束符位置
         for(c=i,f=HT[i].parent;f!=0;c=f,f=HT[f].parent)    //从叶子到根逆向求编码
         {   
              if(HT[f].lchild==c) cd[--start]='0';
              else
                   cd[--start]='1';
         }
     HC[i]=(char*)malloc((n-start)*sizeof(char)); //为第i个字符编码分配空间
     strcpy(HC[i],&cd[start]);//从cd复制编码到HC
     }
  free(cd);   //释放工作空间
}
 
 
void main(){
int n,i;
int* w;//记录权值
char* ch;//记录字符
HuffmanTree HT;
HuffmanCode HC;
printf("请输入你要进行编码的字符个数");
scanf("%d",&n);
w=(int*)malloc((n+1)*sizeof(int));//记录权值,0号单元未用
ch=(char*)malloc((n+1)*sizeof(char));//记录字符,0号单元未用
printf("依次输入待编码的字符和权值:");
for(i=1;i<=n;i++){
printf("data[%d]=",i);
scanf("%s",&ch[i]);
printf("weight[%d]=",i);
scanf("%d",&w[i]);
}
 
HuffmanCoding(HT,HC,w,n);
for(i=1;i<=n;i++)
printf("%c:%s\n",ch[i],HC[i]);
 
}

WPL计算方法:
字符出现的概率,乘上编码长度,加和即可

第七章 图

图的定义和术语

顶点 弧 边
有向图的叫弧
无向图的是边

名词和术语 所代表的含义
弧或边带权的图分别称作有向网或无向网。
子图 顶点集和弧集分别是原来图的子集的,称为子图
完全图 假设图中有 n 个顶点,e 条边,则含有 e=n(n-1)/2 条边的无向图称作完全图(即每个顶点都两两相连)含有 e=n(n-1) 条弧的有向图称作 有向完全图;
稀疏图,稠密图 假设图中有 n 个顶点,e 条边,若边或弧的个数 e<nlogn,则称作稀疏图,否则称作稠密图。
邻接点 假若顶点v 和顶点w 之间存在一条边,则称顶点v 和w 互为邻接点
边(v,w) 和顶点v 和w 相关联。和顶点v 关联的边的数目定义为顶点v的度。
出度、入度 对于有向图而言,顶点的出度: 以顶点v为弧尾的弧的数目;顶点的入度: 以顶点v为弧头的弧的数目。顶点的度(TD)=出度(OD)+入度(ID)
路径,路径长度 设图G=(V,{VR})中的一个顶点序列\({ u=v_{i,0},v_{i,1}, …, v_{i,m}=w}中,(v_{i,j-1},v_{i,j})∈VR,1≤j≤m\)则称从顶点u 到顶点w 之间存在一条路径。路径上边的数目称作路径长度。
简单路径 序列中顶点不重复出现的路径。
简单回路 序列中第一个顶点和最后一个顶点相同的路径。
连通图 若图G中任意两个顶点之间都有路径相通,则称此图为连通图
连通分量 若无向图为非连通图,则图中各个极大连通子图称作此图的连通分量。
强连通图 对有向图,若任意两个顶点之间都存在一条有向路径,则称此有向图为强连通图。
强连通分量 否则,其各个强连通子图称作它的强连通分量。
生成树 假设一个连通图有 n 个顶点和 e 条边,其中 n-1 条边和 n 个顶点构成一个极小连通子图,称该极小连通子图为此连通图的生成树。
生成森林 对非连通图,则称由各个连通分量的生成树构成的集合为此非连通图的生成森林。

本章节涉及期末考试的内容较多,大部分是大题

很多题目喜欢给一个图以及算法,让我们写出算法的最终运算结果,需要对本章节的算法有一个较为熟练的掌握

图的数组表示法

用两个数组分别存储数据元素(顶点)和数据元素之间的关系(边或弧)的信息。
邻接矩阵

#define INFINITY   INF_MAX    //最大值∞         在考试的时候统一定义为无穷大,实际运用应当结合实际分析
#define  MAX_VERTEX_NUM 20     //最大顶点个数
Typedef enum{DG,DN,UDG,UDN} GraphKind;//{有向图、
                     //有向网、无向图、无向网}
typedef struct ArcCell { // 弧的定义
     VRType  adj;    // VRType是顶点关系类型。对无权图,用1
          //或0表示相邻否;对带权图,则为权值类型。
     InfoType  *info;  // 该弧相关信息的指针
} ArcCell,  AdjMatrix[MAX_VERTEX_NUM] [MAX_VERTEX_NUM];
typedef struct { // 图的定义
     VertexType  vexs[MAX_VERTEX_NUM];   // 顶点向量
     AdjMatrix    arcs;            // 邻接矩阵                     
      int    vexnum, arcnum;   // 图的当前顶点数和弧数      
      GraphKind   kind;     // 图的种类标志             
  } MGraph;
  
  Status CreateGraph( MGraph &G ) {
     //采用数组(邻接矩阵)表示法,构造图G.
     scanf(&G.kind);
     switch(G.kind)  {
       case DG:return CreateDG(G);  //构造有向图G
       case DN:return CreateDN(G);  //构造有向网G
       case UDG:return CreateUDG(G);  //构造无向图G
       case UDN:return CreateUDN(G);  //构造无向网G
       default:return ERROR;
    }
}// CreateGraph
Status CreateUND( MGraph &G ) {
     //采用数组(邻接矩阵)表示法,构造无向网G.
     scanf(&G.vexnum,&G.arcnum,&IncInfo); 
     // IncInfo为0则各弧不含其他信息
     for(i=0; i<G.vexnum; ++i)    
             scanf(&G.vexs[i]);
     for(i=0; i<G.vexnum; ++i)
         for(j=0; j<G.vexnum; ++j) 
              G.arcs[i][j]={INFINITY,NULL};
     for(k=0; k<G.arcnum; ++k)  {   
          //构造邻接矩阵
         scanf(&v1, &v2, &w);
          //输入一条边依附的顶点及权值
         i=LocateVex(G, v1); j=LocateVex(G, v2);
         G.arcs[i][j].adj=w; 
         if(IncInfo) Input(*G.arcs[i][j].info); 
         G.arcs[j][i]=G.arcs[i][j]; }
         return OK;
                  }// CreateUDN

图的邻接表表示法

邻接表
弧的结点结构

typedef struct ArcNode {  
  int        adjvex;   // 该弧所指向的顶点的位置
  struct ArcNode  *nextarc; 
                             // 指向下一条弧的指针
  InfoType   *info;   // 该弧相关信息的指针
} ArcNode;

顶点的结点结构

typedef struct VNode { 
  VertexType  data;   // 顶点信息
  ArcNode  *firstarc; 
                   // 指向第一条依附该顶点的弧
  } VNode, AdjList[MAX_VERTEX_NUM];

图的结构定义

typedef struct {  
     AdjList  vertices;
     int    vexnum, arcnum; //图的当前顶点数和弧数 
     int    kind;          // 图的种类标志
  } ALGraph;

image
image
image

图的遍历!!!

深度优先搜索

从图中某个顶点V0 出发,访问此顶点,然后依次从V0的各个未被访问的邻接点出发深度优先搜索遍历图,直至图中所有和V0有路径相通的顶点都被访问到;若此时图中尚有顶点未被访问,则另选图中一个未曾被访问的顶点作起始点,重复上述过程,直到图中所有顶点都被访问到为止
image

void DFS(Graph G, int v) {
   // 从顶点v出发,深度优先遍历图 G
    visited[v] = TRUE;   VisitFunc(v);//访问第v个顶点
    for(w=FirstAdjVex(G, v);
             w!=0; w=NextAdjVex(G,v,w))
        if (!visited[w])  DFS(G, w);
              // 对v的尚未访问的邻接顶点w
              // 递归调用DFS
} // DFS
广度优先搜索

从图中的某个顶点V0出发,并在访问此顶点之后依次访问V0的所有未被访问过的邻接点,之后按这些顶点被访问的先后次序依次访问它们的邻接点,直至图中所有和V0有路径相通的顶点都被访问到。
若此时图中尚有顶点未被访问,则另选图中一个未曾被访问的顶点作起始点,重复上述过程,直至图中所有顶点都被访问到为止。

void BFSTraverse(Graph G,  Status (*Visit)(int v)){
 //按广度优先非递归遍历图G,使用辅助队列Q和
//访问标志数组visited
  for (v=0; v<G.vexnum; ++v)
      visited[v] = FALSE;  //初始化访问标志
  InitQueue(Q);       // 置空的辅助队列Q
  for ( v=0;  v<G.vexnum;  ++v )
     if ( !visited[v]) {          // v 尚未访问
     visited[u] = TRUE;  Visit(u);    // 访问u
EnQueue(Q, v);             // v入队列
while (!QueueEmpty(Q))  {
  DeQueue(Q, u);        
                         // 队头元素出队并置为u
  for(w=FirstAdjVex(G, u); w>=0; 
                         w=NextAdjVex(G,u,w))
     if ( ! visited[w])  {  //W为u的尚未访问的邻接顶点
        visited[w]=TRUE;  Visit(w);
        EnQueue(Q, w); // 访问的顶点w入队列
     } // if
      } // while
 } // BFSTraverse

最小生成树

生成树:是一个极小连通子图,它含有图中全部顶点,但只有n-1条边。
最小生成树:如果无向连通图是一个带权图,那么它的所有生成树中必有一棵边的权值总和为最小的生成树,称这棵生成树为最小代价生成树,简称最小生成树。
构造最小生成树的准则

  • 必须只使用该网络中的边来构造最小生成树;
  • 必须使用且仅使用n-1条边来联结网络中的n个顶点;
  • 不能使用产生回路的边。
普里姆算法

Prime算法特点: 将顶点归并,与边数无关,适于稠密网。
普里姆(Prim)算法思想
令集合U的初值为U={u0},集合T={}。从所有结点u∈U和结点v∈V-U的带权边中选出具有最小权值的边(u,v),将结点v加入集合U中,将边(u,v)加入集合T中。如此不断重复,当U=V时,最小生成树便构造完毕。
要构造一个辅助数组,对当前V-U集中的每个顶点,记录和顶点集U中顶点相连接的代价最小的边。对每个顶点vi ∈V-U,在辅助数组中存在一个相应分量closedge[i-1],它包括两个域,其中lowcost存储该边上的权
image

克鲁斯卡尔算法

Kruskal算法特点:将边归并,适于求稀疏网的最小生成树。
先有顶点,然后往里面加边,从权值最小的开始往里面加,如果新加入的边会使图里面产生回路,则舍去。

拓扑排序

由某个集合上的一个偏序得到该集合上的一个全序,这个操作称之为拓扑排序。
进行拓扑排序的步骤:

  • 从有向图中选取一个没有前驱的顶点并输出
  • 从有向图中删去此顶点以及所有以它为尾的弧

重复以上两步,直至图空,或者图不空但找不到无前驱的顶点为止。后一种情况说明有向图中存在环。

关键路径

关键活动”指的是:该弧上的权值增加 将使有向图上的最长路径的长度增加。
求法:

  • 求出拓扑排序
  • 求出逆拓扑排序
  • 求出事件的最早发生时间(顶点)
  • 求出事件的最迟发生时间
  • 求出活动的最早发生时间e(弧)
  • 求出活动的最迟发生时间t
  • 求出每个活动中 t - e 的值
  • t - e 的值为0的活动的集合就是关键路径

最短路径Dijkstra算法

求单源最短路径
基本步骤

  • 定义一个数组D[v],表示从源点 s 到顶点 v 的边的权值,如果没有边则将D[v]置为无穷大
  • 把图的顶点集合划分为两个集合 S 和 V-S。第一个集合 S 表示距源点最短距离已经确定的顶点集,即一个顶点如果属于集合 S 则说明从源点 s 到该顶点的最短路径已知。其余的顶点放在另一个集合 V-S 中。
  • 每次从尚未确定最短路径长度的集合 V-S 中取出一个最短特殊路径长度最小的顶点 u,将 u 加入集合 S,同时修改数组 D 中由 s 可达的最短路径长度。若加入集合 S 的 u 作为中间顶点时,vi 的最短路特殊路径长度变短,则修改 vi 的距离值(即当D[u] + W[u, vi] < D[vi]时,令D[vi] = D[u] + W[u, vi])
  • 重复第 3 步的操作,一旦 S 包含了所有 V 中的顶点,D 中各顶点的距离值就记录了从源点 s 到该顶点的最短路径长度

我的实验四中涉及到的dijstra算法

void DijkstraShort(ALGraph G, int pre);//寻找最短路径的算法
void DijkstraShort(ALGraph G, int pre) {
	InitializeShortest(G, pre);//初始化shortest数组
	
	int temNode = 0;//暂时存放由FindMin找出来的顶点在shortest内的位置消息
	int temLenght = 0;//暂时存放中转点到另外一个点的距离信息

	//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
	temNode = FindMin(G);
	while (temNode != -1) {
		temLenght = 0;//初始化
		if (temNode) {
			//G.verties[shortest[temNode].adjvex].judge = 1;//标记成已经访问过
			node[countNode] = shortest[temNode].adjvex;//进顶点集
			countNode++;
			p = G.verties[shortest[temNode].adjvex].firstarc;
			while (p) {
				temLenght = temLenght + shortest[temNode].lenght + p->info->length;
				if (shortest[FindNode(p->adjvex, pre)].lenght > temLenght || shortest[FindNode(p->adjvex, pre)].lenght == -1) {
					shortest[FindNode(p->adjvex, pre)].lenght = temLenght;
					ModificationShortest(temNode, FindNode(p->adjvex, pre));
				}
				p = p->nextarc;
				temLenght = 0;
			}
		}//还能找到
		
		temNode = FindMin(G);
		temLenght = 0;//初始化
		
	}
	

}

第九章 查找

静态查找表

  • 折半查找

查找算法的平均查找长度ASL
为确定记录在查找表中的位置,需和给定值进行比较的关键字个数的期望值。
\(ASL = \sum \limits_{i = 1}^n P_iC_i\)
\(n\)为表长,\(P_i\)为查找表中查找第 i 个记录的概率,且\(\sum P_i = 1\), \(C_i\)为找到该记录时,和给定值比较过的关键字的个数

折半查找代码:

int Search_Bin ( SSTable ST, KeyType key ) {
//在有序表ST中折半查找其关键字等于key的数据元素。
//若找到,则函数值为该元素在表中的位置,否则为0。
   low = 1;  high = ST.length;     // 置区间初值
   while (low <= high) {
      mid = (low + high) / 2;
      if (EQ (key , ST.elem[mid].key) )
        return  mid;        // 找到待查元素
      else  if ( LT (key , ST.elem[mid].key) )
        high = mid - 1;       // 继续在前半区间进行查找
      else  low = mid + 1; // 继续在后半区间进行查找
   }
   return 0;                 // 顺序表中不存在待查元素
} // Search_Bin

image
一般情况下,表长为 n 的折半查找的判定树的深度和含有 n 个结点的完全二叉树的深度相同。
n > 50 时,\(ASL_{bs} \approx log_2(n + 1) - 1\)
注意:折半查找只适用于顺序结构,且关键字必须是有序的

动态查找表

二叉排序树和二叉平衡树

定义:二叉排序树或者是一棵空树;或者是具有如下特性的二叉树:

  • 若它的左子树不空,则左子树上所有结点的值均小于根结点的值;
  • 若它的右子树不空,则右子树上所有结点的值均大于根结点的值;
  • 它的左、右子树也都分别是二叉排序树。

二叉排序树的插入删除操作

  • 插入 先查找是否拥有该元素,如果没有直接插入即可
  • 删除 如果是叶子,直接删除,如果只有左孩子或者右孩子,继承,如果两边都有,找前驱或者后继继承(前驱:左边树最大的 后继:右边树最小的)

平衡二叉树
平衡二叉树又称AVL树,是二叉排序树的另一种形式。它或者是一棵空树,或者是具有下列性质的二叉树:

它的左子树和右子树都是平衡二叉树,且左子树和右子树的深度之差的绝对值不超过1。

  • 结点的平衡因子:
    该结点的左子树高度减去它的右子树高度。即:
    \(h_l - h_r\)

  • 平衡二叉树的特点为:
    树中每个结点的左、右子树深度之差的绝对值不大于1。即:
    \(|h_l - h_r| \leq 1\)
    image

  • 平衡二叉树插入方式:采用平衡旋转技术

    • LL
      image

    • LR
      image

    • RR
      image

    • LR
      image

例题1:输入关键字序列为 { 16, 3, 7, 11, 9, 26, 18, 14, 15 },构成一棵平衡二叉排序树。
image
image

例题2:给出如下平衡树,平衡二叉树中删除结点22,展示输出后的结果。
image
image

B-树插入删除

定义
image
注意:!!!不能瘸腿!

阶的概念
对于一棵m阶B-tree,每个结点至多可以拥有m个子结点。
即遍观整棵树,子节点最多的个数是m,那么这棵树就是m阶树。
image
树的度
树的度就是树的高度,即树的层数。
image

b树删除规则:
分下列几种情况

  1. 要删除的键位于叶中。有两种情况。
    • 删除健不会违反节点应持有的最小健数的属性。
      image
    • 删除键违反了节点应持有的最小键数的属性。在这种情况下,我们按照从左到右的顺序从紧邻的兄弟节点借用一个键。
      image
      如果两个直接同级节点的键数都已达到最小值,则将该节点与左侧同级节点或右侧同级节点合并,这个合并是通过父节点完成的。
      image
  2. 如果要删除的键位于内部节点中,则会发生以下情况。
    • 如果左子节点的键数超过最小值,则删除的内部节点将替换为中序前置节点。
      image
    • 如果右子节点的键数超过最小值,则删除的内部节点将替换为中序后置节点。
    • 如果任一子级的键数正好是最小的,则合并左子级和右子级。
      image
  3. 在这种情况下,树的高度会缩小。如果目标键位于内部节点中,并且删除该键会导致节点中的键数量减少(即,少于所需的最小值),则查找中序前置和中序后置。如果两个子项都包含最少数量的键,则不能进行借用。这导致了第二种情况(3),即合并孩子。
    再说一遍,找兄弟同级借键。但是,如果同级也只有最少数量的键,则将节点与同级以及父级合并。把孩子们按顺序排列好。
    image

哈希表

定义
根据设定的哈希函数 H(key) 和所选中的处理冲突的方法,将一组关键字映象到一个有限的、地址连续的地址集 (区间) 上,并以关键字在地址集中的“象”作为相应记录在表中的存储位置,如此构造所得的查找表称之为“哈希表”。

第十章 内部排序

插入排序

直接插入排序

假设待排序的记录存放在数组R[1..n]中,排序过程的某一中间时刻,R被划分成两个子区间R[1..i-1] 和R[i..n],其中:前一个子区间是已排好序的有序区;后一个子区间则是当前未排序的部分,不妨称为无序区。直接插入排序的基本操作是将当前无序区的第一个记录R[i]插入到有序区中适当的位置。

  • 设置监视哨
  • 不设置监视哨
折半插入排序
快速排序
选择排序

堆排序

  • 大顶堆
  • 小顶堆

大致思路:
如果想从大到小排序,就建立小顶堆,顶和最后一个数字交换,再建立小顶堆(排除以及被交换过的尾部数据)
建立小顶堆的时候,有筛选这个环节
从上往下,依次交换不符合小顶堆的数据

归并排序

自己写的归并,大致的思路就是这个

#include <iostream>
using namespace std;

int a[20] = {2,1,8,12,15,16,11,5,14,6,7,4,13,3,19,18,9,17,10,20};
int a1[20] = {};

void S(int pre1, int next1, int pre2, int next2);
void S(int pre1, int next1, int pre2, int next2) {
	int i = pre1;
	int j = pre2;
	int tem = pre1;
	while (i <= next1 && j <= next2) {
		
		if (a[i] < a[j]) {
			a1[tem] = a[i];
			tem++;
			i++;
		}
		else {
			a1[tem] = a[j];
			tem++;
			j++;
		}
	}
	while (i <= next1) {
		a1[tem] = a[i];
		i++;
		tem++;
	}
	while (j <= next2) {
		a1[tem] = a[j];
		j++;
		tem++;
	}
	for (int m = pre1; m <= next2; m++) {
		a[m] = a1[m];
	}
}
void Sort(int pre, int next);
void Sort(int pre, int next) {
	int tem = 0;
	tem = (pre + next) / 2;
	if (pre >= next) {
		return;
	}
	Sort(pre, tem);
	Sort(tem + 1, next);
	S(pre, tem, tem + 1, next);
}

void main() {
	Sort(0, 19);
	for (int i = 0; i < 20; i++) {
		cout << a[i]; 
		cout << endl;
	}
}
基数排序

需要掌握最低位优先法

各种排序方法的综合比较

posted @ 2023-06-22 23:31  中原白也  阅读(117)  评论(0)    收藏  举报