[数据结构复习]自用大纲

内容多基于王道和李春葆《数据结构教程》,做复习提纲之用

队列

队列的应用是缓冲区、页面替换算法,递归、进制转换和迷宫求解是栈的应用。

  • 循环队列是空间的循环使用,基于线性表

顺序

队列是线性表(具有逻辑上的前驱后继关系)。头插尾删,先进先出

队列的实现至少需要维护如下内容(一数组,二指针):

  • 数组(定长,且知道大小)
  • 队首指针front(指向队首元素)
  • 队尾指针rear(指向队尾元素的下一个位置)

队列的逻辑是环形使用数组,数组最后一个位置使用后接着使用第一个位置。通过对下标使用取余确保不会越界。

由于空队列时的条件为front==rear,若不加以修改,满队列条件实际上和这个一样,因此有三种策略:

  1. 少装一个元素,当front == (rear+1)%MaxSize就认为满
  2. 多设置一个size变量,入队自增,出队自减
  3. 设置tag,入队置1,出队置0

对3进行解释:只有在前一次操作插入才会导致队满,此时tag==1,故有判定条件为front == rear && tag == 1。同理只有在前一次操作删除才会导致队空满,此时tag==0,故有判定条件为front == rear && tag == 01

注意:

  • 入队rear++前需先判断是否满
  • 出队front++前需先判断是否空

链队

一般需要维护的如下:

  • 链表节点(一般需要头结点,空的,初始化malloc一个就行)
  • 两个指针(指向头结点和尾节点,front == reat为空)

链队一般不会满(除非没得内存),但是顺序队由于底层是定长数组,会满。

要点:

  • 入队就尾插
  • 出队就front->next
  • 删除节点记得释放空间

此外还有双端队列和循环队列,过于简单不做赘述

最适合做队列的是带队首和队尾指针的非循环单链表。带队首指针的循环单链表为什么不行呢?因为循环链表判断表空(判断首尾)麻烦。最不适合做链式队列的是只带队首指针的非循环双链表(查找到队尾需要 O ( n ) O(n) O(n)

后进先出

典型应用:

  • 括号匹配
  • 树的某些遍历方法
  • 前中后缀表达式
  • 函数调用有调用栈

实现需要:

  • 定长数组
  • 记录栈顶下标的int变量top

中缀转后缀

23王道数据结构 96页11题 栈 中缀表达式转后缀表达式

其实要点也很简单,就是从左到右顺次扫描,遇到数直接输出,遇到操作符要和栈进行操作。

对于操作符:

  • 若栈空直接入栈
  • 若栈不空需要和栈顶元素比较优先级
    • 若当前的优先级大于栈顶则入栈
    • 若当前的优先级小于等于栈顶则栈顶出栈且重复至大于最新栈顶,随后入栈
    • 左括号直接入栈
    • 右括号则出栈至左括号

所有出栈操作均是要把操作符写在表达式结果尾部

package main

import (
	"fmt"
	"strings"
)
var lvl = map[rune]int{
// 存储运算符优先级
'*':2,
'/':2,
'+':1,
'-':1,
}
func f(s string)string{
	var stack [10]rune
	ptr := 0
	var res strings.Builder
	for _,i := range s{
		if (i >= '1' && i <= '9' ) || (i >= 'a' && i <= 'z' ){
			res.WriteRune(i)
		}else{
			// 处理字符是符号的情况
			if i == '(' {
				// 左括号直接入栈
				stack[ptr] = i
				ptr+=1
			}else if ptr == 0{
				// 栈空直接入栈
				stack[ptr] = i
				ptr+=1
			}else if lvl[i] > lvl[stack[ptr-1]]{
				// 若当前运算符优先级大于栈内的则入栈
				stack[ptr] = i
				ptr+=1
			}else if i == ')'{
				// 字符是右括号则出栈至左括号
				for stack[ptr-1]!='('{
					res.WriteRune(stack[ptr-1])
					ptr -= 1
				}
				ptr -= 1
			}else {
				// 当前的优先级小于等于栈顶则栈顶出栈
				for ptr > 0 && lvl[i] <= lvl[stack[ptr-1]] {
					res.WriteRune(stack[ptr-1])
					ptr -= 1
				}
				stack[ptr] = i
				ptr+=1
			}

		}
	}
	// 栈中剩余字符弹出
	for ptr > 0 {
		res.WriteRune(stack[ptr-1])
		ptr -= 1
	}
	return res.String()
}

func main() {
	//fmt.Println(f("1+(2*3)-2/7"))
	fmt.Println(f("a+b-a*((c+d)/e-f)+g"))
}

运行上述代码,能得到答案ab+acd+e/f-*-g+


当然普通做题就简单了,一般是两种方法

  1. 画一棵表达式树,一眼就出结果
  2. 根据运算符的优先级将所有算术单元括起来,然后将操作符号移到相应的括号之后并除去括号

如对于a*(b+c)-d可以补全括号为((a*(b+c))-d),然后变成((a(bc)+)*d)-,之后再去掉括号变成abc+*d-

数组

高维数组

数组A[4][7][3][5]每个元素占一个单位空间,首地址从0开始,则a[1][3][2][1]地址为161,计算方法是(1-0)*(7*3*5)+(3-0)*(3*5)+(2-0)*(5)+1=161

矩阵压缩

涉及:

  • 压缩前后的下标转换
  • 上/下三角矩阵
  • 稀疏矩阵

稀疏矩阵的压缩存储结构:

  • 三元组( m ∗ n m*n mn的矩阵当三元组大小 < ( m ∗ n ) / 3 <(m*n)/3 <(mn)/3才有意义)
  • 压缩存储就不能通过下标来和首元素位置来计算某元素的位置(即不能随机存取)
  • 十字链表法

注:

  1. 稀疏矩阵压缩存储后会失去随机存取的特性
  2. 矩阵下标从1开始,数组若无例外则下标从0开始。若有类似A[1...n]这样的则是从1开始

[ADT笔记]串(String)及其C语言实现

串是特殊的线性表(线性表在逻辑上具有前驱和后继的关系)

实现:

#define maxsize 50
typedef struct
{
	char data[maxsize];
	//使用length可以把求长度的时间复杂度降为O(1)
	int length;
}SqString;

核心是char[],但是为了知道串长度有如下几种操作:

  1. 另设一个存放长度的int(上例即是)
  2. data[0]存放长度(由于char占一个字节,所以串长不能超过256)
  3. 使用\0结尾,不存串长(那Getlength()无法降到 O ( 1 ) O(1) O(1),上两种方法倒是可以)

注:串可以链式存储,但是每个节点存储一个char回导致存储密度降低。可以一个节点存一个块(一个块存多个字符)形成块链结构,缺点是增删麻烦。

其他:模式匹配问题(蛮力/KMP)

  • 节点的度:节点有几个孩子
  • 树的度:各节点的度的最大值

度为n的树至少有一个节点的度为n,但是n叉树允许所有节点度小于n,甚至可以是空树

二叉树:

  • 二叉树左右顺序要区分
  • 满二叉树:叶子结点全在最下面一层,非叶子结点均有两个子节点
  • 完全二叉树:深度为h的二叉树除第 h 层外其它各层 (1~h-1) 的结点数都达到最大个数,第 h 层所有的结点都连续集中在最左边
  • 实现可以顺序存储或者链式存储

遍历一般依靠递归就够了:

  • 前序:根左右
  • 中序:左根右
  • 后序:左右根

这里可以和上面提到的前中后缀表达式联系起来

如:*优先级大于-,可以直接入栈,

线索二叉树:

线索二叉树的前提必须指明是那种遍历方式

就是在指定的遍历方式的情况下,节点会产生前驱/后继的关系(因为遍历会得到一个访问序列)。线索二叉树就是要为节点指明前驱后继,该过程称为线索化。

线索化要使用节点的空的指针,也就是说需要线索化处理的节点位于叶子结点或者孩子没满的节点。

typedef struct BiTree {
	struct BiTree *lchild, *rchild;	//左右子节点指针
	int data;	// 数据域
	// 左右标志,0 代表指向的是孩子,1代表指向的是前驱/后继
	int LTag;
	int RTag;
}BiNode;

对于这些指针,如果是左指针空,则用它指向前驱,且LTag置1;若右指针为空,则用它指向后继,且置RTag为1。通过Tag变量区分指向的到底是子节点还是前驱后继。

线索化时需要借助中变量temp保存前驱以供访问。这里还是得看代码才直观

森林或树转换到二叉树

  1. 森林中各个树的根结点之间视为兄弟关系
  2. 左孩子右兄弟存储

二叉搜索树BST

[算法笔记]树表的查找:BST与AVL

  1. 若根节点的左子树非空,则左子树上所有结点关键字均小于根节点关键字
  2. 若根节点的右子树非空,则右子树上所有结点关键字均大于根节点关键字
  3. 根节点的左、右子树本身又各是一棵二叉排序树

BST涉及平衡操作,AVL树就是一种平衡的二叉搜索树,涉及LL、RR、LR、RL四种过程。

具体做题时,由底向上寻找第一个不平衡的子树进行旋转调整。找到最长树杈上的根节点开始的三个节点摆平衡,然后其余的加进来即可

平衡二叉树:每个节点左右子树高度最多相差1

哈夫曼树

哈夫曼树叶子结点有n个,则非叶子结点有n-1个,没有度为1的节点

[ADT笔记]图(graph)

生成树:所有顶点均由边连接在一起但不存在回路的图

表示方法:

  • 邻接矩阵
  • 邻接表
  • 十字链表

十字链表(链式)较之于三元组 (顺序)更适合(非零元素)节点的增删

查找

  • n个元素二分查找最多比较次数是 log ⁡ 2 n + 1 \log_2n+1 log2n+1向上取整
  • 堆的查找操作低效

哈希

数据结构—— 构造散列函数的六种方法【直接定址法-数字分析法-平方取中法-折叠法-除留余数法-随机数法】

构造哈希函数的方法:

  • 直接定址法
  • 除留余数法
  • 数字分析法
  • 平方取中法
  • 折叠法

但是要和哈希冲突的解决方法区分开:

  • 开放寻址法
    • 线性探查
    • 二次探查
    • 伪随机探测
  • 链地址法(拉链法)

排序

在这里插入图片描述

[算法笔记]排序算法1:插入、选择和交换排序

[算法笔记]排序算法2:桶、计数与基数排序

[算法笔记]二叉堆

  • 堆排序执行一趟可以直接确定最大值/最小值
  • 冒泡排序执行一趟可以直接确定最大值/最小值
  • 直接选择排序每一趟只能确定元素在当前已选的元素集合中的位置
  • 快速排序执行一趟可以直接确定枢轴位置

广义表

学过lisp的都知道 carcdr操作

  • 广义表元素可以是表,tail取的是表,head取的是元素(可以是表)
  • 空的广义表:不含任何元素
  • 广义表长度:最大括号中逗号数目+1

其他补充

单链表 O ( 1 ) O(1) O(1)前插

bool InsertPriorNode(LNode *p, LNode *s){
    if(p==NULL || S==NULL)
        return false;
    // 常规尾插连接节点后交换数据域
    s->next = p->next;
    p->next = s;  // s连接到p
    ELemType temp = p->data;  // 交换数据域部分
    p->data = s->data;
    s->data = temp;

    return true;
}

二叉树与度为2的有序树的区别

有序树的定义:若将树中每个结点的各子树看成是从左到右有次序的(即不能互换),则称该树为有序树(Ordered Tree)
无序树的定义:若将树中每个结点的各子树从左到右是没有次序的(即可以互换),则称该树为无序树

因此对于问题“度为二的有序树是不是二叉树”,显然回答“不是”

  • 度为2的树至少有3个结点,而二叉树可以为空
  • 度为2的有序树的孩子的左右次序是相对于另一个孩子而言的,若某个结点只有一个孩子,则这个孩子就无须区分其左右次序,而二叉树无论其孩子数是否为2,均需确定其左右次序,即二叉树的结点次序不是相对于另一个结点而言,而是确定的。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值