一、树与树的表示
1.查找
静态查找,查找的对象集合本身不发生改变,例如查字典;动态查找,查找的对象集合本身是动态变化的。
顺序查找
将数据储存在数组里,按照顺序进行查找。此外需要一个结构指针Tbl
,它的Data域是数组中元素的个数;指针域指向这个数组。需要存n个数据,就把数组大小设为a[n+1]。因为要设置一个哨兵a[0] = K,用来终止循环判断是否查找到K,a[1]到a[n]用来储存数据。
struct LNode{
int length;
int Arr[MaxSize];
};
typedef struct LNode* List;//结构指针
Search(int K;List Tbl)//假设要找的元素是K
{
List->Arr[0] = K;
for(int i = List->Tbl;List->Arr[i] != K;i--);//注意是从索引大向着索引小的方向循环遍历查找K
return i;
}
没有哨兵的话,循环结束,意味着找到了K或者超出了数组的边界。for(int i = List->length; List->Arr[i] != K && i>=0;i--)
这样就会复杂些。
哨兵的作用就是我们再循环的时候不需要每次去判断他的下标,少写一个判断分支。而且只要循环结束,可以马上知道是否有元素K。返回0表示数组中没有元素K;返回其他的表示查找成功。
注意循环是从大索引向小索引查找。
这样的查找方法效率不高
二分查找
- 二分查找可以实现的前提:
1数据存放在数组里面;
2数据在数组中是有序排放的,降序或者升序。 - 举例:
因为mid = 100 < 444,所以就可以确定444在右半部分。所以舍弃左半部分,对右半部分重复进行操作。
代码
从a[0]开始储存数据,Search()函数查找K,并返回K所在的位置下标;Sort函数进行冒泡排序;
void Sort(int a[], int length);
int Search(int a[], int length, int K);
int main()
{
int a[] = { 9,5,2,4,3, };
int length = sizeof(a) / sizeof(a[0]);
Sort(a, length);
printf("排序之后的数组:\n");
for (int i = 0; i < length; i++)
{
printf("%d ", a[i]);
}
printf("\n");
int four = Search(a, length, 4);
int non = Search(a, length, 7);
int nine = Search(a, length, 9);
printf("4的下标在%d\n", four);
printf("9的下标在%d\n", nine);
printf("7的下标在%d\n", non);
return 0;
}
void Sort(int a[],int length)//length 链表的长度
//由于冒泡排序的特殊性,无法对部分元素进行排序,要排就全都排
{
int i, j;
for (i = 0; i < length - 1; i++)
{//第一次层循环控制冒泡排序 冒泡的轮数
//length个数,需要冒泡length-1次,每次冒泡就会完成对一个数的排序
for (j = 0; j < length - 1 - i; j++)
{//第二次循环控制本轮冒泡需要比较的次数
//这里i表示已经完成排序的元素的个数,每轮需要比较的元素个数依次减小;
if (a[j] > a[j + 1])
{//交换两个元素的值
int temp = a[j];
a[j] = a[j + 1];
a[j + 1] = temp;
}//完成升序排序
}
}
}
int Search(int a[], int length, int K)
{//二分查找的函数
int left = 0,right = length -1;
while (left <= right)//判定条件:不越界
{
int middle = (left + right) / 2;
if (K > a[middle])//因为是升序排序,说明在右半部分,变left
{
left = middle + 1;
}
else if (K < a[middle])
{
right = middle - 1;
}
else
return middle;
}
return -1;
}
时间复杂度:O(logN)
二分查找的判定树
11个元素,1到11进行查找,使用判定树进行模拟。第一次查找肯定是将K与6进行比较,判断是在左半部分还是在右半部分,依次进行下去…
第一次查找可找到6;第二次查找可找到3,9;第三次查找可找到1,4,7,10…
- 这个树一共有四层(从上到下),如果要查找4,那么久需要查找3次,因为4在第三层;
- ASL(平均寻找次数) = (需要找4次的数字个数 + 需要找3次的数字个数 + 需要找2次的数字个数+…)/ 数字总个数;
使用层次化的结构储存数据:查找树的形式,原理类似二分查找,使得查找过程更加方便,当在树中查找节点、删除节点的时候,会比数组方便得多,可以更好地处理动态查找问题。
2.树的定义和术语
定义
类似递归的思路,一个树有多个子树组成,每个树都有根Root…
- 每个子树之间互不相交!
- 除了根节点之外,每个节点有且只有一个父节点;
- 一棵有N个节点的树有N-1条边。因为除了根节点A,每个节点都只有向上的一条边。
- 树是保证节点联通的边最少的一种连接方式。
术语
例如,右图中的树:
- 节点的度为3,因为根节点A有三个子树;
- 树的度为3,看最多的子树数目:A和D最大度数;
- 叶节点,即没有子树的结点:FLH…
右图中树的深度为4;
3.树的表示
使用数组实现起来相对困难;如果使用链表表示树,那么就需要每个节点的指针个数都是相同的,会造成较大的空间上的浪费。
那么就可以使用儿子兄弟表示法:
将他旋转45度,可以看成一个树,每个节点都有两个指针,一个指向左边,一个指向右边。这种树叫做二叉树,度为2的一种数。
一般的树就可以使用二叉树链表的形式来实现。
二、二叉树及储存结构
1.二叉树的定义及性质
二叉树的定义
1.二叉树的左子树和右子树两不相交。
2.二叉树可以是空树,可以只有一个结点,可以只有左子树,可以只有右子树,可以有左右子树。
3.注意二叉树是一种度为2的树,子树有左右顺序之分。而一般的度为2的树,子树是没有左右之分的。
特殊二叉树
1.斜二叉树,就是单链表;
2.完美二叉树(满二叉树),对称完美,除了最后一层都有两个孩子结点。
- 完全二叉树
如果一个完全二叉树有k层,那么它的前k-1层都是满的完美形态,它的最底层允许在右边缺少连续的结点。完美二叉树是完全二叉树的特例。
二叉树的性质
- 1.二叉树第i层最多有2的i-1次方 个节点。
- 2.深度为k层的二叉树具有的最大节点总数为2的k次方-1 个节点。
完美二叉树即为结点最多的情况。
最大节点总数,就是k层所有的节点等比求和
- 3.对于非空的二叉树,叶节点个数比度为2(有两个儿子)的结点个数多1。
n0表示叶节点的个数;n1表示只有一个儿子的结点个数;n2表示有两个儿子的结点。关系为n0 = n2 + 1。(可以简单的理解为度为2的节点提供了两个子节点的位置)
证明:使用两种对总数边的求法
- 4.具有N个节点的完全二叉树的深度一定为[log2 N] +1。
理解:最后一个叶节点的父节点一定是最后一个非叶节点(这个节点之后的节点全部都是叶节点)
- 5.如果深度为d的二叉树只有度为0和2的结点,那么这个二叉树的节点数至少为2d-1。
理解:每层的节点只有一个度为2的结点,另一个节点为叶节点。也就是说,除了第一层的根节点,每一层只有两个节点。
二叉树的抽象数据类型定义
其中对二叉树的遍历是最基本的操作。常见的遍历方法有四种。
2.二叉树的存储结构
顺序储存结构
对于完全二叉树(比较适合):
使用数组来连续存储完全二叉树,即使用对结点编号的方式来对应数组元素的下标。
a[n+1] 表示可以储存序号从1 到 n,已知任何一个结点,都可以进行访问操作:
- 查找父节点:a[i] 的父节点为 a[i/2] ,注意i/2取整
- 左孩子:a[i] 的左孩子是a[2i];
- 右孩子: a[i] 的右孩子是a[2i + 1];
- 如果下标索引超出n,表示越界,所找的节点不存在。
对于一般的二叉树(空间浪费):
索引访问的对应关系仍然适用!但是会造成空间浪费。比如这个例子,只有五个节点有数据,却需要使用13个数组空间来储存。
链式储存
节点不存在的情况使用NULL来表示。