选择什么数据结构,要看你执行什么样的操作
栈: empty push pop 都是 O(1)
队列: enqueue dequeue O(1)
实例:
数值转换: 十进制和d进制转换 N=(N div d)*d+N%d
括号匹配的检验
行编辑程序 (实现退字符和退行功能)
迷宫求解:穷举法中,完成路径的回退,有点类似于深度遍历
表达式求解:有两个栈 OPTR 和 ORTD,思路如下:
置OPTD为空栈,OPTR栈底为#
若为操作数则进栈,若为运算符则从OPTD中取出数字运算后,将结果入栈
匹配: KMP算法, 为O(m+n),不需要回溯
insert(L ,x ) delete(L, x) x为元素
查找比较费时,为 O(n)
实例:
一元多项式
表示: ( (p1,e1), (p2,e2)... (pn,en)) 利用e有序的列表存储
相加: for i 从 1到n
获取 a和b ei对应系数,不存在返回0
将系数相加
相乘: A(x)*B(x)=求和 bi*A(x)*x^i
有多种表示方法,选择哪一种按实际情况,如:
左孩子,右兄弟表示法: 每个节点有指向左孩子和右兄弟的指针
父节点表示法:只有向上遍历的操作
数组:表示完全二叉树,堆
左右孩子节点: 二叉树的表示方法
1. 字典操作是 insert delete 和search,一种好的数据结构是散列表
2. 当容器容量m大于数据个数n 时,直接将关键字对应到下标,值对应到数组值即可
3. n个元素插入长为m的集合,其装载因子是 n/m ,. 解决碰撞的算法中,链接法比较实用
除法散列: k mod m 其中 m为散列表的大小选择与2的幂不太接近的质数
乘法散列: m(kA mod 1)
全域散列:从一组散列函数中随机地选择一个作为散列函数
随机化保证了对于任何输入,都有较好的平均性能
使用链接法解决碰撞问题
一种设计:设H为一组散列函数,它将给定的关键字域U映射到{0,1,…,m-1}中,这样的一个函数组称为是全域的。如果从H中随机地选择一个散列函数,当关键字K≠J时,两者发生碰撞的概率不大于1/m。
常用的一个全域散列函数类:首先选择一个足够大的质数p ,使得每一个可能的关键字k 都落到0 到p-1 的范围内,包括首尾的0 和p-1。这里我们假设全域是0 – 15,p 为17。设集合Zp 为{0, 1, 2, „, p-1},集合Zp* 为{1, 2, 3, „, p-1}。由于p 是质数,我们可以定义散列函数h(a, b, k) = ((a*k + b) mod p) mod m。其中a 属于Zp,b 属于Zp*。由所有这样的a 和b 构成的散列函数,组成了函数簇。即全域散列。
随机性应该这样理解:对于某一个散列表来说,它在初始化时已经把a,b固定了,但是对于一个还未初初始化的全域散列表来说,a,b是随机选取的。
将所有元素都放在散列表中,节省了指针的存储,减少了碰撞,坏处是有可能被填满
一个元素放在 散列表的什么位置,是由探查函数说了算的,探查函数 h(k,i) 代表关键字k 第i次探查的位置,i从0到m-1. 形成一个序列,直到将 k 放入散列表中。有三种探查函数:
线性探查 i 每次加一
二次探查 每次 i的平方
双重散列 (h(k)+i*g(k)) mod m 是最好方法之一,因为它所产生的排列具有随机选择的排列的许多特性。
对散列元素的删除操作执行起来比较困难,因为删除操作会影响查找操作。解决办法是在槽里的值被删除后置一个特定的值DELETED,而不是删除后不管,查找的时候处理一下就可以了
可以设计更好的散列方法,使得最坏情况下查找为 O(1),实现完全散列。
一种两级的散列方案,每一级都采用全域散列。
为了确保在第二级上不出现碰撞,需要让第二级散列表Sj的大小mj为散列到槽j中的关键字数nj的平方,则存储总量的期望小于2n。
完全散列的关键在于:二次散列表中要求没有碰撞。这是通过确保二级散列表中能够槽的个数是关键字的个数的平方来实现的。
1. 多种动态集合操作: search minimum maximum predecessor successor insert 和delete , 综合了优先队列和字典的功能。
2. 使用二叉查找树,以上操作时间为 O(树的高度)
3. B树对维护随机访问的二级磁盘的数据库非常有效
4. 二叉查找数的定义 key(left(x) ) < key(x)< key(right(x))
中序遍历:递归左子树,自己,递归右子树 O(n) 得到排序结果
查找:
search(x, k) {
while(x!=null && x.key!=k) {
if(x.key
x=x.left;
} else
x=x.right;
}
return x;
}
最大和最小关键字: 遍历到最左和最右子节点
while(x.left!=null) { x=x.left; }
return x;
前趋和后继 x的前趋不会有右子树
precessor(x) {
if( x.left!=null) {
return maxinum(x.left);
}else {
while(x.parent!=null && x.parent.right!=x ) { //向上查找到根或者右分支的存在
x=x.parent;
}
return x.parent;
}
}
插入
insert(T,z)
if(T.root==null) {
T.root=z; //树为空
}else {
x=T.root
while(x!=null) {
y=x;
if(z.key< x.key)
x=x.left;
else
x=x.right;
}
z.parent=y;
if(y.key>z.key)
y.left=z;
else
y.right=z;
}
删除
deleteNodeNullBranch(T,z)
修改 z.parent 指向z的指针为null
deleteNodeOneBranch(T z)
修改 z.parent 指向z 的指针指向 z的分支
delete(T,z)
if(z.rigth==null && z.left==null)
deleteNodeNullBranch (T,z);
else if(z.left !=null && z.right!=null)
y=successor(z);
copy y.key into z.key
deleteNodeOneBranch(T, y)
else
deleteNodeOneBranch(T, y)
6. 随机构造更接近于最佳情况,一棵在n个关键字上随机构造的二叉查找树的期望高度为O(lgn)。
7. 可以使用孩子-兄弟链表来存放树,则该树的先序和中序类似于二叉链表
8. 四皇后问题:使用树画出其分布情况
1. 二叉查找树上的操作时间复杂度是 O(树的高度),而树最好的高度是 lgn,红黑树可保证最坏情况下,树的高度也是lgn
2. 红黑树是一种近似平衡的二叉查找树,定义
1)根为黑色
2)无连续的红节点,红节点个数为 <=h/2
3)节点每棵子树的黑节点数目相同
3. 红黑树的高度最多为 2lg(n+1)
4. 当运行delete和insert时,左旋和右旋通过改变颜色和指针结构 ,保证红黑树性质。
x 和 y 间的键断掉, x和y的高度交换
重新链接 x和y的 子节点
按 BST 插入节点 z
将 z 着成红色 满足 性质3,有可能破坏
性质1: 当树为空时会发生,直接修改z的颜色
性质2: 当新节点被插入到红节点下时,调整如下:
z 的叔父节点为 红色:变换 叔父、父亲和祖父的颜色,z指向 祖父节点
z 的叔父是黑色,z是 z.parent的右孩子:z指向z. parent ,z 和 z.right 左旋(调整为左孩子)
z 的叔父是黑色,z是z.parent的左孩子: 变换父亲和祖父的颜色,父亲和祖父右旋
按BST 删除节点z
如果 z是黑色,调整如下:
z的兄弟w是红色: 变换 兄弟w和父亲的颜色,并在w和z.parent间,对p.parent执行左旋
z的兄弟w是黑色,且w的两个孩子都是黑色: 兄弟变红,z指向父亲
z的兄弟w是黑色,w左红右黑: 交换 w和其左孩子颜色,并右旋
z的兄弟w是黑色,w右红: 变换 w w.parent w.right的颜色,并对w执行右旋
7. AVL树和红黑树的比较
AVL增删时需要旋转的次数更多,现实中红黑树用的更多
1. 实际工程中一般是对标准数据增加一些属性和操作来满足需求,这些属性必须可通过数据结构的常规操作维护
选择基础数据结构
确定要添加的属性
验证可用 常规操作来维护这些属性
添加新的操作
3. 红黑树的扩张: 新增属性可以由该节点和左右子树来决定,那么插入和删除中对红黑树的这些附加信息来维护不影响时间复杂度。
4. 动态顺序统计
在动态集合上 O(lgn)的时间内得到 x的排名; 得到排名为i的元素
通过对红黑树进行扩扩展,为每个节点加入信息 size[x]=size[left[x]]+ size[right[x]]+1
5. 区间树
元素不是一个值,而是一个区间。使用 low 作为key, 子树最大的high作为扩展域