1 引言
1.1 数据结构
集合结构->线性结构->树形结构->图型结构
关系的存储
- 顺序存储
- 链接存储
- 哈希存储方式
- 索引存储方式
1.2 算法分析
时间复杂度:渐进分析法,大O表示法
1.3 最大连续子序列问题
找到和最大的连续序列
- 穷举法:O(n3)
- 优化掉最里层循环O(n2)
- O(n)开始子序列和不可能是负的,遍历一遍即可过滤掉不可能的情况。
3 栈
后进先出的线性表
3.1 栈的顺序实现
利用动态数组,栈底下标为0,用一个整型数储存栈顶的位置
3.2 栈的链接实现
将单链表的头指针指向栈顶
3.3 栈的应用
递归消除
括号配对
计算器
4 队列
先进先出的线性表
4.1 队列的顺序实现
队头位置固定:队头下标为0,出队操作后面的元素都要向前移动
队头位置不固定:入队出队操作是O(1),但是浪费空间
循环队列:规定front指向的单元不存储队列
4.1 队列的链接实现
单链表的表头是队头,单链表的表尾是队尾,保存一个链接队列只需要两个指向单链表结点指针front和rear
当队列为空时,front和rear均为NULL
5 树
5.1 树的概念
树的定义:
有一个根节点,其余节点可分成若干个互不相交的树
不含任何节点的是空树,由若干互不相交的树的集合是森林
一个结点的直接后继的数目称为结点的度
5.2 二叉树
5.2.1 基本概念
二叉树:二叉树或者为空,或者由根节点和互不相交的左右子树构成,且左右子树都是二叉树
满二叉树:一个高度为k并具有2k-1个结点的二叉树称为满二叉树。
完全二叉树:满二叉树最底层从右到左去掉若干个结点。
注意只有根节点的二叉树不是空二叉树,而且二叉树需严格区分左右子树。
5.2.2 二叉树的遍历
前序遍历:根节点->左子树->右子树
中序遍历:左子树->根节点->右子树
后序遍历:左子树->右子树->根节点
顺序遍历:一层一层的来
仅有前序遍历和后序遍历的结果无法确定唯一的二叉树。
5.2.3 二叉树的顺序实现
利用完全二叉树按层编号的性质,编号可表现父子关系
缺点:对于非完全二叉树,比较费空间
5.2.4 二叉树的链接实现
二叉链表
三叉链表:存放父节点、左右儿子结点
5.3 树的应用
5.3.1 表达式树
5.3.2 哈夫曼树和哈夫曼编码
由于在哈夫曼树中只有叶子结点和度为2的结点,对于n个待编码的元素,哈夫曼树中共有2n-1个结点,可以通过静态数组储存;struct结点储存待编码元素、结点权重、父节点儿子节点的下标位置。
申请一个长度为2n的数组,0表示树根,暂时不用;n个待编码的元素放大后n个位置
5.4 树和森林
5.4.1 树的存储实现
- 树的标准储存:二叉树的扩展,一个儿子用一个指针储存
- 儿子链表表示法:表头数组+链表表示
- 儿子兄弟链表表示法:将任意一棵树表示成二叉树的形态
- 双亲表示法:通过指向父节点的指针将树中的所有结点组织在一起
5.4.2 树的遍历
- 前序遍历
- 后序遍历
- 层次遍历
5.4.3 树、森林和二叉树
和儿子兄弟链表表示法相似,将任意一个森林或者任意一棵树都能表示成二叉树
6 优先级队列
6.1 二叉堆
- 最小化堆:根节点最小
- 二叉堆的存储:顺序存储,利用数组下标反应父子关系与大小关系
6.1.1 优先级队列需要的操作
create():建堆,如果看成n次插入需要O(nlogn)的时间复杂度#
enquene():新元素入队
dequene():优先级最高的元素出队
getHead():返回头结点
isEmpty()
dequene过程中,定义向下过滤的函数percolateDown(int n),将最后一个结点换到根节点,将根节点和两个儿子中最小的一个比较,交换父子,一直到子节点为止。
6.2 D堆
为什么一般用二叉堆而不是D堆?
乘2除2用左移右移就好了,其他乘除会慢
为什么还会有人用D堆?
数据存在外存中,D堆可以减少读取外存次数
6.3 二项堆(贝努里树)
- 归并:类似二进制加法,O(logn)
- 入队:入队是归并的特例,相当于二进制加法中加一,最坏的时间复杂度是O(logn)
- 出队:找到根最小的树O(logn),拆散的树再归并就好了
6.4 多服务台的排队系统
发生时间早的事件先处理,发生时间晚的后处理,用优先级队列存放事件,方式时间早的优先级高
事件驱动的仿真:每次优先级队列吐出一个事件:
到达事件:直接服务(生成离开时间)或者排队
离开事件:排队队列少一个元素或者空闲柜台+14
7 集合和静态查找表
7.1 集合的定义
集合中的元素没有任何逻辑关系。
键值/关键字值:集合元素的唯一标识
7.2 查找表
- 查找表:用于查找的集合
- 静态查找表:数据元素个数和每个数据元素的值不变,用数组即可
- 动态查找表:会动态变化,要进行插入删除操作
- 内部查找:数据在内存
- 外部查找:数据在外存
7.3 无序表的查找
无序表:数组中的元素是无序的。
无序表的查找:顺序查找O(n)的复杂度
改进的顺序查找只需要n+1次查找,而不是2n。
7.4 有序表的查找
- 顺序查找 O(n)
- 二分查找
- 插值查找
为什么不用插值查找?
计算一次运算量太大,不如二分查找右移
为什么还会有人用插值查找?
当数据在外存时,可以减少读取数据的次数 - 分块查找(索引查找)
类似于字典,利用索引表,先确定大致位置
8 动态查找表
需要支持查找、增、删操作,线性表不适合动态查找表。
查找树:用于处理动态查找表的树
8.1 二叉查找树(二叉排序树)
左子树的所有元素<根节点<右子树所有元素
中序遍历二叉查找树即可得到递增次序
- find:查找,O(logn)
- insert:插入,先执行查找算法,找出被插结点的父亲结点,再插入为左右结点O(logn)
- remove:删除,也是O(logn)
如何删除?
叶子结点和只有一个子节点的很容易删除
对于有两个子节点的结点,用左子树的最大值作为替身,因为左子树的最大值一定没有右儿子。 - 当二叉查找树极不平衡时,退化成单列表,为线性复杂度。
8.2 AVL树(平衡二叉查找树)
- 平衡因子(平衡度):结点的平衡度是结点左子树高度-右子树高度
- AVL树:左右子树高度最多差一层
每个结点需要保存平衡信息 - find
- insert:有可能会影响平衡性,旋转一下就好了,最多只用调整一个结点
LL/RR:
LR/RL:
删除操作:删除后不平衡,需要调整,之后高度变矮,可能父节点也要调整,最后一直可能都要调整。
如果要删除结点x,有可能x是叶节点,有可能x只有一个儿子,有可能x有两个儿子。前两种情况x的父节点的相应子树的高度减一。
当x有两个儿子时,用右子树的最小结点或者左子树的最大结点代替x即可。
8.3 伸展树
90-10规则:90%的访问都是针对10%的数据
向根旋转策略:被访问的数据元素向父节点旋转,朝根节点移动。
希望自底向上的伸展使树更加平衡,针对不同的情况有三种不同的旋转:zig,zig-zig和zig-zag
8.3.1 zig
8.3.2 zig-zag
8.3.2 zig-zig
9 散列表
哈希法:也称散列法,根据所求结点的关键字值key直接直接找到这个结点,时间复杂度为O(1)
散列函数(hash function):将一个项映射成一个较小的下标的函数,即定义域大,值域小
哈希函数的要求:计算速度快,散列地址尽可能均匀,使冲突的概率降低
9.1 哈希函数:
直接地址法
取模运算(用得最多):p最好是质数,这样更均匀一些
数字分析法
平方取中法:4731*4731=22,382,361,所以取中间两位82
折叠法:对于542,242,241,542+242+241=1025,所以取025
9.2 冲突解决
- 线性探测法:当散列发生冲突时,探测下一个单元,直到发现一个空单元
- 二次探测法
- 再次散列法
- 开散列表:链地址法:
9.3 闭散列表类
支持三个操作:插入、查找和删除
必须提供一个指向函数的指针,从数据元素中提取关键词字段并转换成整型数
删除:迟删除,只是做一个标记
9.4 二次探测法
地址序列为k+12,, k+22,
如果采用二次探测法,而且表的大小是一个素数,那么如果表至少有一半是空的,新元素总能被插入,而且在插入过程中没有一个单元被探测两次。
这个表还要存储负载因子这个参数
9.5 开散列表
指针数组+不带头节点的单链表实现
10 排序
- 内排序:数据元素全部存放在计算机的内存之中
- 外排序:数据元素主要存放在外存储器
排序的大致分类: - 插入排序
- 选择排序:把最小元素选择出来,一个一个拿出来
- 交换排序:不另外申请空间,光交换
- 归并排序:利用分治法
- 基数排序
10.1 插入排序
- 直接插入排序
- 折半插入排序:查找采用二分查找,但是搬家还是一个一个搬,所以时间复杂度还是O(n2)
- 希尔排序:对于相隔距离为gap的元素进行排序,直到gap=1,
为什么希尔排序交换效率比直接插入排序高?
因为插入排序最好的情况就是不操作,到最后序列越来越有序,应用希尔增量,最坏的时间复杂度是O(n2),平均时间复杂度是O(n3/2)
10.2 选择排序
依次选出最小的元素
- 直接选择排序
时间复杂度O(n2) - 堆排序
建堆用O(n)的时间,基于堆的优先级队列选出最小元素只需要O(logn),所以总的时间是O(nlogn),没有最好最坏情况
空间问题:需要另申请n个空间吗?
不需要,每次dequeue之后堆缩小1,可以在堆的最后一个位置存储刚被删去的元素。
template <class KEY, class OTHER>
void heapSort(SET<KEY, OTHER> a[], int size){
int i;
SET<KEY, OTHER> tmp;
// 创建初始的堆
for( i = size / 2 - 1; i >= 0; i-- )
percolateDown( a, i, size );
//执行n-1次deQueue
for ( i = size - 1; i > 0; --i) {
tmp = a[0]; a[0] = a[i]; a[i] = tmp; //delete a[0]
percolateDown( a, 0, i );
}
}
template <class KEY, class OTHER>
void percolateDown(SET<KEY, OTHER> a[], int hole, int size){
int child;
SET<KEY, OTHER> tmp = a[ hole ];
for( ; hole * 2 + 1 < size; hole = child){
child = hole * 2 + 1;
if( child != size - 1 && a[ child + 1 ].key > a[ child ].key )
child++;
if( a[ child ].key > tmp.key ) a[ hole ] = a[ child ];
else break;
}
a[ hole ] = tmp;
}
10.3 交换排序
- 冒泡排序:O(n2)
第i次起泡需要n-i个次比较,最坏n-i次交换,最坏需要n-1次起泡。 - 快速排序:分治法,递归实现,
在待排序的序列中选择一个标准元素,把所有数据元素分成两组
如何选择标准元素?
序列第一个元素:对于增序列或者减序列会变成直接选择排序
随机选一个元素
或者选择序列的中间值
时间复杂度:最坏是O(n2),平均情况和最好情况是O(nlogn)
10.4 归并排序:
分治法:先划分再归并,没有最好或者最坏的情况
10.5 基数排序:非比较的算法
设待排序的元素组成一个不带头节点的单链表,每个口袋也用一个不带头节点,可以达到近似线性的时间复杂度
template <class OTHER>
struct node {
SET<int, OTHER> data;
node *next;
node() { next = NULL; }
node(SET<int, OTHER> d): data(d)
{ next = NULL; }
};
template <class OTHER>
void bucketSort(node<OTHER> *&p){ // p是链表头
node<OTHER> *bucket[10], *last[10], *tail ;
int i, j, k, base = 1, max = 0, len = 0;
for (tail = p; tail != NULL; tail = tail->next) // 找最大键值
if (tail->data.key > max) max = tail->data.key;
// 寻找最大键值的位数
if (max == 0) len = 0;
else while (max > 0) { ++len; max /= 10; }
for (i = 1; i <= len; ++i) { // 执行len次的分配与重组
for (j = 0; j <= 9; ++j) bucket[j] = last[j] = NULL;
while (p != NULL) { // 执行一次分配
k = p->data.key / base % 10;
if (bucket[k] == NULL) bucket[k] = last[k] = p;
else last[k] = last[k]->next = p;
p = p->next;
}
p = NULL; // 重组后的链表头
for (j = 0; j <= 9; ++j) { // 执行重组
if (bucket[j] == NULL) continue;
if (p == NULL) p = bucket[j];
else tail->next = bucket[j];
tail = last[j];
}
tail->next = NULL; // 表尾置空
base *= 10; // 为下一次分配做准备
}
}