数据结构
Posted yanhui007
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了数据结构相关的知识,希望对你有一定的参考价值。
二叉树
二叉树每个节点都大于左子节点,小于右子节点。
平衡二叉树
二叉树进行更新操作后可能导致不平衡:如图,插入10后,11节点左边高度为3,右边为1,差大于1了。就要对树进行旋转使树保持平衡。
旋转分4种情况:
如图:观察发现失衡的节点为10,新插入的节点是5,5处于10的左子节点(7)的左子节点(4)下边,这种情况叫做左左。
依次还有左右、右左、右右
左左:右旋
左右:先对7进行左旋,树变成左左树了,然后对11进行右旋
右右和右左同理。
删除
上面将的是新增节点之后通过旋转维持平衡。
删除相对插入要复杂一些。
(1)当删除的节点是叶子节点,则将节点删除,然后从父节点开始,判断是否失衡,如果没有失衡,则再判断父节点的父节点是否失衡,直到根节点,此时到根节点还发现没有失衡,则说此时树是平衡的;如果中间过程发现失衡,则判断属于哪种类型的失衡(左左,左右,右左,右右),然后进行调整。
(2)删除的节点只有左子树或只有右子树,这种情况其实就比删除叶子节点的步骤多一步,就是将节点删除,然后把仅有一支的左子树或右子树替代原有结点的位置,后面的步骤就一样了,从父节点开始,判断是否失衡,如果没有失衡,则再判断父节点的父节点是否失衡,直到根节点,如果中间过程发现失衡,则根据失衡的类型进行调整。
(3)删除的节点既有左子树又有右子树,这种情况又比上面这种多一步,就是中序遍历,找到待删除节点的前驱或者后驱都行,然后与待删除节点互换位置,然后把待删除的节点删掉,后面的步骤也是一样,判断是否失衡,然后根据失衡类型进行调整。
遍历
前序:根 - 左 - 右
中序:左 - 根 - 右
后续:左 - 右 - 根
234树
如图中:
W节点叫2-node
MO节点叫3-node
FGL节点叫4-node
插入
新插入的数据总是插入到叶子节点上。
向2-node插入数据后,2-node就变为3-node,同理3-node变4-node。
但是当向4-node插入数据后,因为4-node无法继续升级,所以就需要分裂。
通常我们会将 4-node中中间的元素,放到它的父节点中,并进行分裂。
分裂是向上分裂的,如果根节点也是4-node怎么办?
这个时候也是继续向上分裂,只不过需要创建新的根节点。
删除
1、如果2-3-4树中不存在当前需要删除的key,则删除失败。
2、如果当前需要删除的key不位于叶子节点上,则用后继key覆盖,然后在它后继key所在的子支中删除该后继key。
什么是后继key?如图,10的后继key就是9。
3、如果当前需要删除的key位于叶子节点上,并且不是2-node,直接删除即可
4、如果当前需要删除的key位于叶子节点上,并且是2-node。又分三种情况
1、如果兄弟节点不是2节点,则父节点中的key下移到该节点,兄弟节点中的一个key上移
2、如果兄弟节点是2节点,父节点是个3节点或4节点,父节点中的key与兄弟节点合并
3、如果兄弟节点是2节点,父节点是个2节点,父节点中的key与兄弟节点中的key合并,形成一个3节点,把此节点看成当前节点(此节点实际上是下一层的节点),重复步骤步骤1和2
第4种情况分析如图:
带有预分裂的插入操作
上面的插入以及删除操作在某些情况需要不断回溯来调整树的结构以达到平衡。为了消除回溯过程,在插入操作过程中我们可以采取预分裂的操作,即我们在插入的搜索路径中,遇到4节点就分裂(分裂后形成的根节点的key要上移,与父节点中的key合并)这样可以保证找到需要插入节点时可以直接插入(即该节点一定不是4节点)。
提前分裂,减少回溯的的查找过程。
带有预合并的删除操作
在删除过程中,我们同样可以采取预合并的操作,即我们在删除的搜索路径中(除根节点,因为根节点没有兄弟节点和父节点),遇到当前节点是2节点,如果兄弟节点也是2节点就合并(该节点的父节点中的key下移,与自身和兄弟节点合并);如果兄弟节点不是2节点,则父节点的key下移,兄弟节点中的key上移。这样可以保证,找到需要删除的key所在的节点时可以直接删除(即要删除的key所在的节点一定不是2节点)。
这里包含key为60的节点也可以选择让父节点中的key 76下移和兄弟节点中的83合并,两种方式都能达到B树的平衡,这也是在2-3-4树对应的红黑树中使用的方式。
实现
数据项类
class DataItem {
public long dData;
public DataItem(long dd) { dData = dd; }
public void displayItem() {System.out.println("/" + dData);}
}
节点类
class Node {
private static final int ORDER = 4;
private int numItems; // 数据项个数
private Node parent; // 父节点
private Node childArray[] = new Node[ORDER]; // 子节点
private DataItem itemArray[] = new DataItem[ORDER-1]; // 节点数据项
public Node getChild(int childNum) {return childArray[childNum];} // 获取当前节点的子节点,通过传入的索引值获取对应的子节点
public Node getParent() {return parent;} // 获取父节点
public boolean isLeaf() {return (childArray[0] == null) ? true : false;} // 判断是否为叶节点
public int getNumItems() {return numItems;} // 获取数据项个数
public DataItem getItem(int index) {return itemArray[index];} // 获取当前节点的数据项,通过传入的索引值获取对应的数据项
public boolean isFull() {return (numItems == ORDER-1) ? true : false;} // 判断该节点的数据项是否已满
// 连接子节点 输入要插入的子节点的位置 还有子节点
public void connectChild(int childNum, Node child) {
childArray[childNum] = child; // 将要插入的子节点放入当前节点的子节点数组的对应的索引的位置上
if (child != null) child.parent = this; // 如果插入的节点不为空的话 还要设置新插入节点的父节点为当前节点
}
// 拆分子节点 输入要拆分的子节点的位置
public Node disconnectChild(int childNum) {
Node tempNode = childArray[childNum]; // 先将要拆分的子节点做一个备份
childArray[childNum] = null; // 然后将当前节点的指定子节点的位置 置空
return tempNode; // 返回备份的节点
}
// 从当前节点中找一个数据项的位置 输入要查找的值
public int findItem(long key) {
for (int j = 0; j < ORDER - 1; j++) { // 遍历当前节点的所有数据项
if (itemArray[j] == null) // 如果当前的位置没有数据了,那么以后的位置也是没有的
break; // 退出当前的循环
else if (itemArray[j].dData == key) // 如果找到了对应的值
return j; // 返回对应大的索引
}
return -1; // 找不到 ,返回-1
}
// 从当前节点插入一个数据项 输入要插入的新的数据项
// 调用该方法的一个前提:当前节点的数据项只有两个或者更少是可以继续插入数据项的(在插入之前会进行一定的检查和判断)
public int insertItem(DataItem newItem) {
numItems ++; // 首先是数据项的个数加1
long newKey = newItem.dData; // 获取到数据项的值
for (int j = ORDER-2; j>=0; j--) { // 遍历
if (itemArray[j] == null) // 如果当前位置的数据项为空的话
continue; // 继续查找
else { // 不为空的话
long itsKey = itemArray[j].dData; // 首先获取当前位置的数据项的值
if (newKey < itsKey) // 如果新的数据项的值小于当前的数据项
itemArray[j+1] = itemArray[j]; // 将当前的数据项放在当前位置的后一个位置上
else { // 否则 要插入的数据项大于当前的数据项
itemArray[j+1] = newItem; // 将新插入的数据项放在当前位置的后一个位置上
return j+1; // 返回插入数据项的索引
}
}
}
// 如果当前节点的数据项为空 或者插入的数据项是和之前已经存在的数据相比是最小的话
itemArray[0] = newItem;// 将当前的数据项插入首位
return 0; // 返回插入的索引值
}
// 删除数据项 返回被删除的数据项 没有指定要删除的位置 所以删除最后一个
public DataItem removeItem() {
DataItem temp = itemArray[numItems - 1]; // 先将当前的数据项中的最后一个做一个备份
itemArray[numItems - 1] = null; // 然后当前节点该处的数据项置为空
numItems --; // 数据个数减1
return temp; // 返回备份
}
// 打印当前节点
public void displayNode() {
for (int j=0; j<numItems; j++) // 遍历并打印所有的数据项
itemArray[j].displayItem();
System.out.println("/");
}
}
234树
public class tree234 {
private Node root = new Node(); // 首先创建一个根
// 查找数据项
public int find(long key) {
Node curNode = root; // 当前访问的节点
int childNumber;
while (true) {
if ((childNumber = curNode.findItem(key)) != -1) // 如果在当前节点中查找对应的数据项返回不为-1,说明找到了对应的数据项
return childNumber; // 直接返回查找数据项对应的索引值
else if (curNode.isLeaf()) // 如果当前节点为叶子节点 则返回-1 说明树中没有改数据项
return -1;
else // 默认情况下 获取下一个节点
curNode = getNextChild(curNode, key);
}
}
// 插入一个数据项
public void insert(long dvalue) {
Node curNode = root; // 找插入位置的时候表示当前的节点的局部变量
DataItem tempItem = new DataItem(dvalue); // 创建一个新的数据项对象
while (true) {
if (curNode.isFull()) { // 如果当前的节点满了的话
split(curNode); // 拆分节点
curNode = curNode.getParent(); // 差分结束之后 之前的节点变为了子节点 所以先获取其父节点 然后重新开始查询
curNode = getNextChild(curNode, dvalue); // 直接查找下一个节点
} else if (curNode.isLeaf()) // 如果当前节点是一个叶子节点,而且未满
break; // 找到了要插入数据的节点 跳出循环 直接进行插入操作
else
curNode = getNextChild(curNode, dvalue); // 没有找到的话 获取下一个子节点节点
}
curNode.insertItem(tempItem); // 让当前的节点插入新的数据
}
// 拆分一个节点 传入一个需要拆分的节点
public void split(Node thisNode) {
DataItem itemB, itemC;
Node parent, child2, child3;
int itemIndex;
itemC = thisNode.removeItem(); // 当前节点中最大的数据项(removeItem方法 默认是删除节点中最大的数据项) 并且已经清空了当前节点的该数据项
itemB = thisNode.removeItem(); // 当前节点中中间的数据项 并且已经清空了当前节点的该数据项
child2 = thisNode.disconnectChild(2); // 当前节点的2号子节点 已经断开了当前节点与2号子节点的连接
child3 = thisNode.disconnectChild(3); // 当前节点的3号子节点 已经断开了当前节点与3号子节点的连接
Node newRight = new Node(); // 新建一个右边的子节点
if (thisNode == root) { // 如果要拆分的节点为根的话
root = new Node(); // 创建一个新的根
parent = root; // 父节点等于新的根
root.connectChild(0, thisNode); // 然后让新的根节点与之前的节点相连 连在最左边的位置上
} else // 不是根的话
parent = thisNode.getParent(); // 先获取要拆分节点的父节点
itemIndex = parent.insertItem(itemB); // 将要拆分节点的中间的数据插入到父节点中 并且获取到插入的索引
int n = parent.getNumItems(); // 获取父节点中数据项的个数
for (int j=n-1; j>itemIndex; j--) { //
Node temp = parent.disconnectChild(j); // 父节点和要拆分的接待你断开连接
parent.connectChild(j+1, temp); // 父节点和要拆分的原节点重新连接 位置为原要拆分节点的中间的数据项在父节点中位置的左边
}
parent.connectChild(itemIndex+1, newRight); // 然后在原要拆分节点新的位置的右边插入新的右边节点
newRight.insertItem(itemC); // 原节点中最大的数据项 插入新的右节点中
newRight.connectChild(0, child2); // 新的右节点和原要拆分节点的右边的两个子节点相连 分别放在新节点的 0 1 位置上
newRight.connectChild(1, child3);
}
// 获取下一个子节点 传入一个当前的节点还有一个要查找的数据项的值
public Node getNextChild(Node theNode, long theValue) {
int j;
int numItems = theNode.getNumItems(); // 获取当前节点的数据项的个数
for (j=0; j<numItems; j++) { // 遍历
if (theValue < theNode.getItem(j).dData) // 如果要查找的值小于当前数据项的值
return theNode.getChild(j); // 返回当前数据项左边的子节点
}
// 如果找不到 则返回最后一个子节点
return theNode.getChild(j);
}
// 打印一整棵树
public void displayTree() {
recDisplayTree(root, 0, 0);
}
// 打印树 传入要从那个节开始打 从那层开始的 哪个节点开始的 前序遍历
private void recDisplayTree(Node thisNode, int level, int childNumber) {
System.out.print("level=" + level + " child=" + childNumber + " "); // 先打印当前节点的状况
thisNode.displayNode();
int numItems = thisNode.getNumItems();
for (int j=0; j<numItems+1; j++) { // 遍历每一个子节点并打印 递归
Node nextNode = thisNode.getChild(j);
if (nextNode != null) // 如果
recDisplayTree(nextNode, level+1, j); // 向下层递归
else
return; // 递归结束
}
}
红黑树
二叉树删除节点的时候,都要遍历到根节点来判断是否失衡,这样效率很低,红黑树通过给节点增加颜色属性来解决此问题。
从上面可以看到,把红色节点放到与父亲齐平,就是2-3树中的一个2-3节点。
红黑树是以空间换时间来减少查询复杂度的数据结构,有以下几种特性:
1、根节点为黑色
2、所有叶子节点为黑色(叶子节点都是不存储数据的如下图)
3、如果一个节点为红色,则它的子节点一定为黑色
4、任意节点,到他任意子节点,经历的黑节点数量一定相同(.红黑树是完美黑色平衡)
当更新操作的时候,上面4种特性可能被破坏,破坏之后要通过颜色调整和旋转来满足上面4个规则。
只要满足上面的条件,就能保证任意节点的最大深度不会超过最小深度两倍(树的深度不会过于失衡)。
旋转
以左旋为例子
红色节点为支点,左旋流程:
1、将黄色节点挂到支点的父节点下面(替换支点)
2、将支点移到黄色节点的左子节点位置
3、将黄色节点的左子节点移动到支点的右子节点位置
插入
那么假设我们在最底部插入元素,肯定是想要插入一个红色结点,这样,树的高度实际上没有任何变化,而插入黑结点使得高度+1。所以插入的链接必定是红色(根节点除外)。
不需要调整的情况:
1、当插入的节点为根节点时,直接将插入节点设置为黑色
2、插入节点的父节点为黑色,不需要任何调整
需要调整的情况:
https://baijiahao.baidu.com/s?id=1623524026892185878&wfr=spider&for=pc
堆
可以把堆看作一个数组,也可以被看作一个完全二叉树,通俗来讲堆其实就是利用完全二叉树的结构来维护的一维数组
按照堆的特点可以把堆分为大顶堆和小顶堆:
大顶堆:每个结点的值都大于或等于其左右孩子结点的值
小顶堆:每个结点的值都小于或等于其左右孩子结点的值
堆常常被当做优先队列使用,因为可以快速的访问到“最重要”的元素
我们对堆中的结点按层进行编号,将这种逻辑结构映射到数组中就是下面这个样子
我们用简单的公式来描述一下堆的定义就是:(可以对照上图的数组来理解下面两个公式)
大顶堆:arr[i] >= arr[2i+1] && arr[i] >= arr[2i+2]
小顶堆:arr[i] <= arr[2i+1] && arr[i] <= arr[2i+2]
堆排序怎么实现?
比如说有个初始数组:
int[] a = {7,3,8,5,1,2};
排序之前堆如下图
通过堆排序实现小顶堆(正序),思路如下:
内层循环:
1、首先找到8位置节点(length/2 - 1),拿着该节点与其子节点(包含左右)分别比较,一旦该节点小于子节点,则替换。
2、接下来比较3节点(8节点 -1),逻辑同上
3、以此类推,第一次遍历后会拿到一个最大值到顶点,因为是正序,所以将顶点与尾点替换。
这样一次循环完毕后,尾节点就遍历出一个最大值。
外层循环:
4、接下来继续循环上面的逻辑,每次循环去掉已经遍历出的尾部节点。
代码实现:
public static void main(String[] args) {
int[] a = {7,3,8,5,1,2};
heapSort(a);
for(int x:a){
System.out.println(x);
}
}
public static void heapSort(int[] a){
for(int j=0;j<a.length;j++){
int length = a.length - j;//外层循环,每次尾部已经遍历过的去掉
for(int i=length/2 - 1;i>=0;i--){
int leftSon = 2*i + 1;//5
int rightSon = leftSon + 1;//6
if(leftSon < length && a[i] < a[leftSon]){
swap(a,i,leftSon);
}
if(rightSon < length && a[i] < a[rightSon]){
swap(a,i,rightSon);
}
}
//首尾替换
swap(a,0,length-1);
}
}
private static void swap(int[] a,int index1,int index2){
int tmp = a[index1];
a[index1] = a[index2];
a[index2] = tmp;
}
以上是关于数据结构的主要内容,如果未能解决你的问题,请参考以下文章
python 用于数据探索的Python代码片段(例如,在数据科学项目中)