[2024深圳市考][计算机素质测试考纲](二)算法和数据结构

前言

因篇幅有限,本文仅对考纲中的考点做基本介绍。

更详细的内容请自行学习:

一、基本概念、数组、链表、栈和队列

数据结构(datastructure)是计算机中存储、组织数据的方式。

不同种类的数据结构适合于不同种类的应用,而部分甚至专门用于特定的作业任务。例如,计算机网络依赖于路由表运作,B-Tree高度适用于数据库的封装。

常见的数据结构有:

  • 栈(Stack):栈是一种特殊的线性表,它只能在一个表的一个固定端进行数据节点的插入和删除操作。
  • 队列(Queue):队列和栈类似,也是一种特殊的线性表。和栈不同的是,队列只允许在表的一端进行插入操作,而在另一端进行删除操作。
  • 数组(Array):数组是一种聚合数据类型,它是将具有相同类型的若干变量有序地组织在一起的集合。
  • 链表(LinkedList):链表是一种数据元素按照链式存储结构进行存储的数据结构,这种存储结构具有在物理上存在非连续的特点。
  • 树(Tree):树是典型的非线性结构,我们通常讨论的是二叉树。
  • 图(Graph):图是另一种非线性数据结构。在图结构中,数据节点一般称为顶点,而边是顶点的有序偶对。
  • 堆(Heap):堆是一种特殊的树形数据结构,一般讨论的堆都是二叉堆。
  • 散列表(Hashtable):散列表源自于散列函数(Hashfunction),其思想是如果在结构中存在关键字和T相等的记录,那么必定在F(T)的存储位置可以找到该记录,这样就可以不用进行比较操作而直接取得所查记录。

数据结构研究的内容就是如何按一定的逻辑结构,把数据组织起来。而算法(algorithm)研究的目的是为了更有效的处理数据,提高数据运算效率。

数据的运算是定义在数据的逻辑结构上,但运算的具体实现要在存储结构上进行。

一般算法会涉及以下几种:

  • 检索:检索就是在数据结构里查找满足一定条件的节点。一般是给定一个某字段的值,找具有该字段值的节点。
  • 插入:往数据结构中增加新的节点。
  • 删除:把指定的节点从数据结构中去掉。
  • 更新:改变指定节点的一个或多个字段的值。
  • 排序:把节点按某种指定的顺序重新排列。例如递增或递减。

二、递归

递归算法是一种直接或者间接调用自身函数或者方法的算法。

通俗来说,递归算法的实质是把问题分解成规模缩小的同类问题的子问题,然后递归调用方法来表示问题的解。它有如下特点:

  • 一个问题的解可以分解为几个子问题的解。
  • 这个问题与分解之后的子问题,除了数据规模不同,求解思路完全一样。
  • 存在递归终止条件,即必须有一个明确的递归结束条件,称之为递归出口。

阶乘就是一个很好的递归例子:

动图

三、树与森林

(一)知识框架

树的知识框架

(二)树的基本概念

树是n(n>=0)个节点的有限集。当n=0时,称为空树。在任意一棵非空树中应满足:

  • 有且仅有一个特定的称为根的节点。

  • 当n>1时,其余节点可分为m(m>0)个互不相交的有限集T1,T2,…,Tm,其中每个集合本身又是一棵树,并且称为根的子树。

显然,树的定义是递归的,即在树的定义中又用到了自身,树是一种递归的数据结构。树作为一种逻辑结构,同时也是一种分层结构,具有以下两个特点:

  • 树的根节点没有前驱,除根节点外的所有节点有且只有一个前驱。

  • 树中所有节点可以有零个或多个后继。

因此n个节点的树中有n-1条边。

(三)术语介绍

下面结合图示来说明一下树的一些基本术语。

在这里插入图片描述

1、祖先、子孙、双亲、兄弟节点

如图所示,考虑节点K。根A到节点K的唯一路径上的任意节点,称为节点K的祖先。如节点B是节点K的祖先,而节点K是节点B的子孙。路径上最接近节点K的节点E称为K的双亲,而K为节点E的孩子。根A是树中唯一没有双亲的节点。有相同双亲的节点称为兄弟,如节点K和节点L有相同的双亲E,即K和L为兄弟。

2、度(degree)、分支节点、叶子节点

树中一个节点的孩子个数称为该节点的度,树中节点的最大度数称为树的度。如节点B的度为2,节点D的度为3,树的度为3。

度大于0的节点称为分支节点(又称非终端节点)。度为0(没有子女节点)的节点称为叶子节点(又称终端节点)。在分支节点中,每个节点的分支数就是该节点的度。

3、深度、高度和层次

节点的层次从树根开始定义。根节点为第1层,它的子节点为第2层,以此类推。双亲在同一层的节点互为堂兄弟,上图中节点G与E、F、H、I、J互为堂兄弟。

节点的深度是从根节点开始自顶向下逐层累加的。

节点的高度是从叶节点开始自底向上逐层累加的。

树的高度(或深度)是树中节点的最大层数。图中树的高度为4。

4、有序树和无序树

树中节点的各子树从左到右是有次序的,不能互换,称该树为有序树,否则称为无序树。假设图为有序树,将子节点位置互换,则变成一棵不同的树。

5、路径和路径长度

树中两个节点之间的路径是由这两个节点之间所经过的节点序列构成的,而路径长度是路径上所经过的边的个数。

注意:由于树中的分支是有向的,即从双亲指向孩子,所以树中的路径是从上向下的,同一双亲的两个孩子之间不存在路径。

6、森林

森林是m(m≥0)棵互不相交的树的集合。森林的概念与树的概念十分相近,因为只要把树的根节点删去就成了森林。反之,只要给m棵独立的树加上一个节点,并把这m棵树作为该节点的子树,则森林就变成了树。

(四)树的性质

树具有以下基本性质:

  • 树中的节点数等于所有节点的度数加1。
  • 度为m的树中第i层上至多有mi-1个节点(i>=1)。
  • 高度为h的m叉树至多有(mh−1)/(m−1)个节点。
  • 具有n个节点的m叉树的最小高度为logm(n(m−1)+1)。

(五)树的存储

在介绍以下三种存储结构的过程中,我们都以下面这个树为例子:

例子

1、双亲表示法

我们假设以一组连续空间存储树的节点,同时在每个节点中,附设一个指示器指示其双亲节点到链表中的位置。也就是说,每个节点除了知道自已是谁以外,还知道它的双亲在哪里。

双亲表示法节点数据结构

其中data是数据域,存储节点的数据信息。而parent是指针域,存储该节点的双亲在数组中的下标。
以下是我们的双亲表示法的节点结构定义代码。

这种表示法的好处是,我们可以根据节点的parent指针很容易找到它的双亲节点,所用的时间复杂度为0(1),直到parent为-1时,表示找到了树节点的根。可如果我们要查找某个节点的孩子是什么,对不起,请遍历整个结构才行。

2、孩子表示法

具体办法是,把每个节点的孩子节点排列起来,以单链表作存储结构,则n个节点有n个孩子链表,如果是叶子节点则此单链表为空。然后n个头指针又组成-一个线性表,采用顺序存储结构,存放进一个一维数组中,如下图所示。

孩子表示法

这样的结构对于我们要查找某个节点的某个孩子,或者找某个节点的兄弟,只需要查找这个节点的孩子单链表即可。对于遍历整棵树也是很方便的,对头节点的数组循环即可。但是,如何知道某个子节点的双亲是谁呢?就不得不通过遍历去查找了。

3、孩子兄弟表示法

我们观察后发现,任意一棵树,它的节点的第一个孩子如果存在就是唯一的,它的右兄弟如果存在也是唯一的。因此,我们设置两个指针,分别指向该节点的第一个孩子和此节点的右兄弟。

节点的结构如下:

在这里插入图片描述

于是通过这种结构,我们就把原来的树变成了这个样子:

在这里插入图片描述

这不就是个二叉树么?

没错,其实这个表示法的最大好处就是它把一棵复杂的树变成了一棵二叉树

(六)二叉树

1、什么是二叉树?

二叉树是另一种树形结构,其特点是每个节点至多只有两棵子树(即二叉树中不存在度大于2的节点)。

与树相似,二叉树也以递归的形式定义。二叉树是n(n≥0)个节点的有限集合:

  • 或者为空二叉树,即n=0。

  • 或者由一个根节点和两个互不相交的被称为根的左子树和右子树组成。左子树和右子树又分别是一棵二叉树。

二叉树是有序树,若将其左、右子树颠倒,则成为另一棵不同的二叉树。即使树中节点只有一棵子树,也要区分它是左子树还是右子树。

2、特殊二叉树

①斜树:所有节点只有左子树的二叉树叫左斜树,只有右子树的称为右斜树。

②满二叉树:一棵高度为h,且含有2h−1个节点的二叉树称为满二叉树,即树中的每层都含有最多的节点。

在这里插入图片描述

③完全二叉树:高度为h、有n个节点的二叉树,当且仅当其每个节点都与高度为h的满二叉树中编号为1~n的节点一一对应时,称为完全二叉树。个人理解,是满二叉树的子集,且有右子树的节点必须有左子树;如果某个节点有子树,其左兄弟节点必是满二叉树。如下图所示:

在这里插入图片描述

④二叉排序树(Binary Sort Tree,BST):左子树上所有节点的关键字均小于根节点的关键字;右子树上的所有节点的关键字均大于根节点的关键字;左子树和右子树又各是一棵二叉排序树。

#图解数据结构:二叉排序树-知乎

⑤平衡二叉树(AVL):树上任一节点的左子树和右子树的深度之差不超过1。

这是一颗平衡二叉树:

img

这颗不是平衡二叉树,其中32号节点左子树深度为4,右子树深度为2,深度差超过了1:

img

⑥线索二叉树:对一棵二叉树中所有节点的空指针域按照某种遍历方式加线索的过程叫作线索化,被线索化了的二叉树称为线索二叉树。

img

img

⑦哈夫曼树:

给定N个权值作为N个叶子节点,构造一棵二叉树,若该树的带权路径长度达到最小,称这样的二叉树为最优二叉树,也称为哈夫曼树(HuffmanTree)。哈夫曼树是带权路径长度最短的树,权值较大的节点离根较近。哈夫曼树又称最优树。

img

若将树中节点赋给一个有着某种含义的数值,则这个数值称为该节点的权。节点的带权路径长度为:从根节点到该节点之间的路径长度与该节点的权的乘积。

img

树的带权路径长度规定为所有叶子节点的带权路径长度之和,记为WPL。

如上图:数的带权路径长度为:WPL=(2+3)3+42+6*1=29。

3、二叉树的存储结构

①顺序存储结构:用一组地址连续的存储单元依次自上而下、自左至右存储完全二叉树上的节点元素,即将完全二叉树上编号为i的节点元素存储在一维数组下标为i−1的分量中。如下图所示,其中0表示并不存在的空节点:

在这里插入图片描述

最坏情况下,一个高度为h且只有h个节点的单支树却需要占据近2h−1个存储单元。

②链式存储结构:既然顺序存储适用性不强,我们考虑下链式存储结构。由于二叉树每个节点最多有两个孩子,所以为它设计一个数据域和两个指针域是比较自然的想法,我们称这样的链表叫做二叉链表。

在这里插入图片描述

其中data是数据域,lchild和rchild都是指针域,分别存放指向左孩子和右孩子的指针。

在这里插入图片描述

容易验证,在含有n个节点的二叉链表中,含有n+1个空链域。

4、二叉树遍历

二叉树的遍历是指从某个节点出发,按照指定次序依次访问二叉树中的所有节点,是的每个节点被访问一次且仅被访问一次。

①先序遍历:1)访问根节点;2)先序遍历左子树;3)先序遍历右子树。如图所示:

在这里插入图片描述

②中序遍历:1)中序遍历左子树;2)访问根节点;3)中序遍历右子树。如图所示:

在这里插入图片描述

③后序遍历:1)后序遍历左子树;2)后序遍历右子树;3)访问根节点。

在这里插入图片描述

5、层次遍历

层次遍历如下图所示:

在这里插入图片描述

层次遍历需要借助队列来实现,其递归算法如下所示:

void LevelOrder(BiTree T){
	InitQueue(Q);	//初始化辅助队列
	BiTree p;
	EnQueue(Q, T);	//将根节点入队
	while(!IsEmpty(Q)){	//队列不空则循环
		DeQueue(Q, p);	//队头节点出队
		visit(p);	//访问出队节点
		if(p->lchild != NULL){
			EnQueue(Q, p->lchild);	//左子树不空,则左子树根节点入队
		}
		if(p->rchild != NULL){
			EnQueue(Q, p->rchild);	//右子树不空,则右子树根节点入队
		}
	}
}

①先将二叉树根节点入队,然后出队,访问出队节点。

②若它有左子树,则将左子树根节点入队;若它有右子树,则将右子树根节点入队。然后出队,访问出队节点。

③如此反复,直至队列为空。

6、由遍历构造二叉树

①由二叉树的先序序列和中序序列可以唯一地确定一棵二叉树。

②同理,由二叉树的后序序列和中序序列也可以唯一地确定一棵二叉树。

③由二叉树的层序序列和中序序列也可以唯一地确定一棵二叉树。

④若只知道二叉树的先序序列和后序序列,则无法唯一确定一棵二叉树。

例如:求先序序列( ABCDEFGH)和中序序列( BCAEDGHFI)所确定的二叉树。

在这里插入图片描述

推导过程:

  • 由先序序列可知A为二叉树的根节点。
  • 中序序列中A之前的BC为左子树的中序序列,EDGHFI为右子树的中序序列。
  • 然后由先序序列可知B是左子树的根节点,D是右子树的根节点。
  • 以此类推,就能将剩下的节点继续分解下去,最后得到的二叉树如图©所示。

7、二叉排序树

二叉排序树的定义已在前文介绍,这里不再赘述。

有趣的是,因为二叉排序树的左子树节点值<根节点值<右子树节点值,所以对其进行中序遍历的时候,就会得到一个递增的有序序列。如下图所示,其中序遍历序列为123468:

在这里插入图片描述

简单构造一个二叉树的结构:

/*二叉树的二叉链表节点结构定义*/
typedef struct BiTNode
{
	int data;	//节点数据
	struct BiTNode *lchild, *rchild;	//左右孩子指针
} BiTNode, *BiTree;

①查找操作:

/*
递归查找二叉排序树T中是否存在key
指针TParentNode指向T的双亲,其初始调用值为NULL
若查找成功,则指针p指向该数据元素节点,并返回TRUE
否则指针p指向查找路径上访问的最后一个节点并返回FALSE
*/
bool SearchBST(BiTree T, int key, BiTree TParentNode, BiTree *p)
{
	if(!T)
    {
		*p = TParentNode;
		return FALSE;
	}
    else if(key == T->data)
    {
		//查找成功
		*p = T;
		return TRUE;
	}
    else if(key < T->data){
		return SearchBST(T->lchild, key, T, p);	//在左子树继续查找
	}
    else
    {
		return SearchBST(T->rchild, key, T, p);	//在右子树继续查找
	}
}

②插入操作:

/*
当二叉排序树T中不存在关键字等于key的数据元素时
插入key并返回TRUE,否则返回FALSE
*/
bool InsertBST(BiTree *T, int key)
{
	BiTree p, newNode;
	if(!SearchBST(*T, key, NULL, &p))
    {
		//查找不成功
		newNode = (BiTree)malloc(sizeof(BiTNode));
		newNode->data = key;
		newNode->lchild = newNode->rchild = NULL;
		if(!p)
        {
			*T = newNode;	//插入newNode为新的根节点
		}
        else if(key < p->data)
        {
			p->lchild = newNode;	//插入newNode为左孩子
		}
        else
        {
			p->rchild = newNode;	//插入newNode为右孩子		
        }
	
        return TRUE;		
    }
    else
    {
		return FALSE;	//树中已有关键字相同的节点,不再插入		
    }
}

以下代码构建一颗二叉排序树:

int i;
int a[10] = {62, 88, 58, 47, 35, 73, 51, 99, 37, 93};
BiTree T = NULL;
for(i = 0; i<10; i++)
{
	InsertBST(&T, a[i]);
}

最终如图所示,其中连线上的序号代表插入的顺序:

在这里插入图片描述

③删除操作:

二叉排序树的查找和插入都很方便,删除操作却比较复杂,因为你要保证删除节点后,仍然是一颗二叉排序树,如下图所示:

在这里插入图片描述

针对要删除的节点同时存在左右子树的情况,让我们考虑一个更简单的例子:

img

此时我们打算删除12这个节点,如上图所示,我们有两种方案:

  • 找到右子树中最小的节点,用它来代替12。
  • 找到左子树中最大的节点,用它来代替12。

不管哪种方案,都能保证替换后,仍然是一颗二叉排序树。以下是第一种方案的代码实现:

/*从二叉排序树中删除节点p,并重接它的左或右子树。*/
bool Delete(BiTree *p){
	BiTree sParentNode, s, tmpNode;
	if(p->rchild == NULL){
		//右子树为空则只需重接它的左子树
		tmpNode = *p;
		*p = (*p)->lchild;
		free(tmpNode);
	}else if((*p)->lchild == NULL){
		//左子树为空则只需重接它的右子树
		tmpNode = *p;
		*p = (*p)->rchild;
		free(tmpNode);
	}else{
		//左右子树均不空
		sParentNode = *p;
		s = (*p)->lchild;	//先转左
		while(s->rchild){//然后向右到尽头,找待删节点的前驱
			sParentNode = s;
			s = s->rchild;
		}
        
         //注意只有在s->rchild为NULL时停止查找,所以s的右子树必然是空的。
		p->data = s->data;	//被删除节点的值替换成它的直接前驱的值
		if(sParentNode != *p){
            //说明s不是p的直接节点
            //s是在p的左子树根节点上,不断地往右子树查找,最终得到的某个节点
            //删除一个没有右子树的节点,只需要将其左子树接到其父节点的右子树上即可
			sParentNode->rchild = s->lchild;	//重接sParentNode的右子树
		}else{
            //sParentNode == *p的时候, s正好是p的左子树的根节点,
            //而s的右子树又是空的,此时要删除s,就把s的左子树接到p的左子树上。
			sParentNode->lchild = s->lchild;	//重接sParentNode的左子树
		}
		free(s);
	}
	return TRUE;
}

二叉排序树的有点明显,查找效率很快,极端情况下,最少为1次,最多也不会超过树的深度,就能查找到对应值。关键是,对于给定的不同序列,按照二叉排序树的定义来说的话,可能会构造出不同的二叉排序树,如下图所示:

在这里插入图片描述

对于左侧树来说,它的时间复杂度为O(logn),近似于折半查找。而右侧的时间复杂度为O(n),其实就是顺序查找。

因此,我们希望给定任何一个序列,都尽可能构建出左侧的这种二叉树,也就是平衡二叉树

8、平衡二叉树

平衡二叉树的定义前面已经讲过,这里不再赘述。我们需要知道的是,如何构建一颗平衡二叉树。

①AVL树

AVL树是严格的平衡二叉树,平衡条件必须满足(所有节点的左右子树高度差的绝对值不超过1)。不管我们是执行插入还是删除操作,只要不满足上面的条件,就要通过旋转来保持平衡。

由于维护这种高度平衡所付出的代价比从中获得的效率收益还大,故而实际的应用不多。只适用于插入与删除次数比较少,但查找多的情况。

广泛应用于Windows NT内核中。

平衡二叉树的插入过程前半部分与二叉排序树一致,但在新节点插入后,若造成查找路径上的某个节点不再平衡,则需要做出相应的调整,主要有以下四种情况:

  • LL平衡旋转,如图所示:

在这里插入图片描述

  • RR平衡旋转,如图所示:

在这里插入图片描述

  • LR平衡旋转,如图所示:

在这里插入图片描述

  • RL平衡旋转,如图所示:

在这里插入图片描述

例如,15,3,7,10,9,8的序列,平衡二叉树的生成过程如下所示:

在这里插入图片描述

②红黑树(Red Black Tree,简称RBT)

RBT也是一颗二叉排序树,只是它在每个节点增加一个存储位表示节点的颜色,可以是红或黑(非红即黑)。通过对任何一条从根到叶子的路径上各个节点着色的方式的限制,红黑树确保没有一条路径会比其它路径长出两倍,也就是:最短路径 × 2 ≥ 最长路径。

因此,红黑树是一种弱平衡二叉树(由于是弱平衡,可以看到,在相同的节点情况下,AVL树的高度必然低于红黑树)。相对于要求严格的AVL树来说,它的旋转次数少,所以对于插入,删除操作较多的情况下,红黑树的效率更佳。

应用场景:

  • C++的STL中
  • Linux的进程调度程序,进程的虚拟内存区域都存储在一颗红黑树上。
  • Linux下IO多路复用的epoll
  • NginX中用红黑树管理定时器
  • Java中TreeMap的实现

img

如上图所示,红黑树具有以下性质:

  • 每个节点要么是红色,要么是黑色。
  • 根节点是黑色的。
  • 哨兵节点(图中NIL代表的部分)和空节点都是认为是黑色的。
  • 不存在两个相邻的红节点,即红节点的父节点和两个孩子节点都是黑色。
  • 对于每个节点x,从该节点到其所有后代叶节点的简单路径(不包含该节点)上,均包含相同数目的黑色节点,称为该节点的黑高,记为bh(x)。

下图中标明了各个节点的黑高(black height):

image-20231225184923136

如果需要平衡处理时,RBT比AVL多一种变色操作,而变色的时间复杂度在O(logN)数量级上,所以在实践中这种变色仍然是非常快速的。

当插入一个节点时,若引起了树的不平衡,AVL和RBT最多都只需要2次旋转操作。但当删除一个节点引起不平衡后,AVL最多则需要logN 次旋转操作,而RBT最多却只需要3次。因此两者插入一个节点的代价差不多,但删除一个节点的代价RBT要低一些。

AVL和RBT的插入删除时的代价主要还是消耗在查找待操作的节点上。

两种算法的时间复杂度基本上都是与O(logN) 成正比的。综合各种业务场景来考虑的话,RBT比AVL适用性更高一些。

9、线索二叉树

传统的二叉链表存储仅能体现一种父子关系,无法不通过遍历就直接得到节点的前驱或者后继。

在这里插入图片描述

如图所示,对于一个有n个节点的二叉链表,每个节点都有左右两个指针指向孩子,因此是2n个指针域。而n个节点的二叉树一共有n-1条线,也就是说存储线的指针域只利用了n-1个,存在着2n-(n-1)=n+1个空指针。

于是我们想到能否利用上这些空指针域,存放上前驱或者后继的节点。这样就能加快访问效率。我们把这种指向前驱和后继的指针称为线索,加上线索的二叉树就称为线索二叉树。

其节点结构如下所示:

在这里插入图片描述

其中:

  • ltag为0时指向该节点的左孩子,为1时指向该节点的前驱。
  • rtag为0时指向该节点的右孩子,为1时指向该节点的后继。

因此对于上图的二叉链表图可以修改为下图的样子:

在这里插入图片描述

10、哈夫曼树

①哈夫曼树的定义和原理

在许多应用中,树中节点常常被赋予一个表示某种意义的数值,称为该节点的。从树的根到任意节点的路径长度(经过的边数)与该节点上权值的乘积,称为该节点的带权路径长度。树中所有叶节点的带权路径长度之和称为该树的带权路径长度,记为:

\[WPL = \Sigma w_il_i \]

式中,wi是第i个叶节点所带的权值,l i该叶节点到根节点的路径长度。
在含有n个带权叶节点的二叉树中,其中带权路径长度(WPL)最小的二叉树称为哈夫曼树,也称最优二叉树。例如,下图中的3棵二叉树都有4个叶子节点a, b,c,d,分别带权7,5,2,4,它们的带权路径长度分别为:

在这里插入图片描述

a. WPL = 7×2 + 5×2 + 2×2 + 4×2 = 36。
b. WPL = 4×2 + 7×3 + 5×3 + 2×1 = 46。
c. WPL = 7×1 + 5×2 + 2×3 + 4×3 = 35。
其中,图c树的WPL最小。可以验证,它恰好为哈夫曼树。

②哈夫曼树的构造

步骤:

  • 先把有权值的叶子节点按照从大到小(从小到大也可以)的顺序排列成一个有序序列。
  • 取最后两个最小权值的节点作为一个新节点的两个子节点,注意相对较小的是左孩子。
  • 用第2步构造的新节点替掉它的两个子节点,插入有序序列中,保持从大到小排列。
  • 重复步骤2到步骤3,直到根节点出现。

看图就清晰了,如下图所示:

在这里插入图片描述

③哈夫曼编码

赫夫曼当前研究这种最优树的目的是为了解决当年远距离通信(主要是电报)的数据传输的最优化问题。哈夫曼编码是一种被广泛应用而且非常有效的数据压缩编码。
比如我们有一段文字内容为“ BADCADFEED”要网络传输给别人,显然用二进制的数字(0和1)来表示是很自然的想法。我们现在这段文字只有六个字母ABCDEF,那么我们可以用相应的二进制数据表示,如下表所示:

在这里插入图片描述

这样按照固定长度编码编码后就是“001000011010000011101100100011”,对方接收时可以按照3位一分来译码。如果一篇文章很长,这样的二进制串也将非常的可怕。而且事实上,不管是英文、中文或是其他语言,字母或汉字的出现频率是不相同的。
假设六个字母的频率为A 27,B 8,C 15,D 15,E 30,F 5,合起来正好是100%。那就意味着,我们完全可以重新按照赫夫曼树来规划它们。
下图左图为构造赫夫曼树的过程的权值显示。右图为将权值左分支改为0,右分支改为1后的赫夫曼树。

在这里插入图片描述

这棵哈夫曼树的WPL为:
W P L = 2 ∗ ( 15 + 27 + 30 ) + 3 ∗ 15 + 4 ∗ ( 5 + 8 ) = 241此时,我们对这六个字母用其从树根到叶子所经过路径的0或1来编码,可以得到如下表所示这样的定义。

在这里插入图片描述

我们将文字内容为“ BADCADFEED”再次编码,对比可以看到结果串变小了。

  • 原编码二进制串: 000011000011101100100011 (共 30个字符)
  • 新编码二进制串: 10100101010111100(共25个字符)

也就是说,我们的数据被压缩了,节约了大约17%的存储或传输成本。

(七)树、森林与二叉树的转化

1、树与二叉树的转化

  • 在兄弟结点之间加一连线;

  • 对每个结点,只保留它与第一个孩子的连线,而与其他孩子的连线全部抹掉;

  • 以树根为轴心,顺时针旋转45°。

如下图所示:

在这里插入图片描述

2、森林与二叉树之间的转化

森林是由若干棵树组成的,所以完全可以理解为,森林中的每一棵树都是兄弟,可以按照兄弟的处理办法来操作:

  • 将森林中的每棵树转换成相应的二叉树;
  • 每棵树的根也可视为兄弟关系,在每棵树的根之间加一根连线;
  • 以第一棵树的根为轴心顺时针旋转45°。

如下图所示:

在这里插入图片描述

注:我看不出这个有什么意义,如果你看中的是二叉树的各种优点,那么你早先业务抽象的时候就应该以二叉树为标准去设计你的数据结构,而非一开始按照实际业务设计出一堆树和森林,然后再转。

四、图

(一)知识框架

在这里插入图片描述

(二)图的基本概念

1.图的定义

  • 在线性表中,数据元素之间是被串起来的,仅有线性关系,每个数据元素只有一个直接前驱和一个直接后继。
  • 在树形结构中,数据元素有明显的层次关系,每一层上的数据元素可以和下一层的多个元素相关,但只能和上一层中的一个元素相关。
  • 在图的结构中,节点的关系可以是任意的,任意两个数据元素中都可以相关。

图(Graph)是由顶点的有穷非空集合V ( G )和顶点之间边的集合E ( G ) 组成,通常表示为: G = ( V , E ),其中,G 表示个图,V 是图 G 中顶点的集合,E 是图G 中边的集合。若V = { v 1 , v 2 , . . . , v n} ,则用∣ V ∣ 表示图G 中顶点的个数,也称图 G 的阶,E = { ( u , v ) ∣ u ∈ V , v ∈ V } ,用∣ E ∣ 表示图 G 中边的条数。

注意:线性表可以是空表,树也可以是空树,但图不能是空图。图中至少有一个顶点,可以没有边。

2.术语

(1)有向图

(2)无向图

(3)简单图

(4)多重图

(5)完全图(又称简单完全图)

(6)子图

(7)连通图、连通分量

(8)强连通图、强连通分量

(9)生成树、生成森林

(10)顶点的度、入度和出度

(11)边的权和网

(12)稠密图、稀疏图

(13)路径、路径长度和回路

(14)简单路径、简单回路

(15)距离

(16)有向树

(三)图的存储结构

1.邻接矩阵

2.邻接表

3.十字链表

4.邻接多重表

5.边集数组

(四)图的遍历

1.深度优先遍历

(1)DFS算法

(2)DFS算法的性能分析

(3)深度优先的生成树和生成森林

2.广度优先遍历

(1)BFS算法

(2)BFS算法性能分析

3.图的遍历与图的连通性

(五)图的应用

1.最小生成树

(1)普里姆(Prim)算法

(2)克鲁斯卡尔(Kruskal)算法

2.最短路径

(1)迪杰斯特拉( Dijkstra )算法

(2)弗洛伊德( Floyd )算法

3.拓扑排序

(1)定义

(2)算法

4.关键路径

(1)定义

(2)算法

五、集合与搜索

六、索引与散列

七、排序

排序算法,就是使得数据按照要求排列的方法。

常见的十种排序算法如下图所示:

img

1、冒泡排序(BubbleSort)

冒泡排序是一种简单的排序算法。它重复地走访过要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。走访数列的工作是重复地进行直到没有再需要交换,也就是说该数列已经排序完成。这个算法的名字由来是因为越小的元素会经由交换慢慢"浮”到数列的顶端。

1.1算法描述

  • 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
  • 对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对,这样在最后的元素应该会是最大的数;
  • 针对所有的元素重复以上的步骤,除了最后一个;
  • 重复步骤1~3,直到排序完成。

1.2动图演示

img

2、选择排序(SelectionSort)

选择排序(Selection-sort)是一种简单直观的排序算法。它的工作原理:首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。

2.1算法描述

n个记录的直接选择排序可经过n-1趟直接选择排序得到有序结果。具体算法描述如下:

  • 初始状态:无序区为R[1..n],有序区为空;
  • 第i趟排序(i=1,2,3…n-1)开始时,当前有序区和无序区分别为R[1..i-1]和R(i..n)。该趟排序从当前无序区中-选出关键字最小的记录R[k],将它与无序区的第1个记录R交换,使R[1..i]和R[i+1..n)分别变为记录个数增加1个的新有序区和记录个数减少1个的新无序区;
  • n-1趟结束,数组有序化了。

2.2动图演示

img  

2.3算法分析

表现最稳定的排序算法之一,因为无论什么数据进去都是O(n2)的时间复杂度,所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。理论上讲,选择排序可能也是平时排序一般人想到的最多的排序方法了吧。

3、插入排序(InsertionSort)

插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。

3.1算法描述

一般来说,插入排序都采用in-place在数组上实现。具体算法描述如下:

  • 从第一个元素开始,该元素可以认为已经被排序;
  • 取出下一个元素,在已经排序的元素序列中从后向前扫描;
  • 如果该元素(已排序)大于新元素,将该元素移到下一位置;
  • 重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
  • 将新元素插入到该位置后;
  • 重复步骤2~5。

3.2动图演示

img

3.3算法分析

插入排序在实现上,通常采用in-place排序(即只需用到O(1)的额外空间的排序),因而在从后向前扫描过程中,需要反复把已排序元素逐步向后挪位,为最新元素提供插入空间。

4、希尔排序(ShellSort)

1959年Shell发明,第一个突破O(n2)的排序算法,是简单插入排序的改进版。它与插入排序的不同之处在于,它会优先比较距离较远的元素。希尔排序又叫缩小增量排序

4.1算法描述

先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,具体算法描述:

  • 选择一个增量序列t1,t2,…,tk,其中ti>tj,tk=1;
  • 按增量序列个数k,对序列进行k趟排序;
  • 每趟排序,根据对应的增量ti,将待排序列分割成若干长度为m的子序列,分别对各子表进行直接插入排序。仅增量因子为1时,整个序列作为一个表来处理,表长度即为整个序列的长度。

4.2动图演示

img

4.3算法分析

希尔排序的核心在于间隔序列的设定。既可以提前设定好间隔序列,也可以动态的定义间隔序列。动态定义间隔序列的算法是《算法(第4版)》的合著者RobertSedgewick提出的。 

5、归并排序(MergeSort)

归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(DivideandConquer)的一个非常典型的应用。将已有序的子序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序表,称为2-路归并。

5.1算法描述

  • 把长度为n的输入序列分成两个长度为n/2的子序列;
  • 对这两个子序列分别采用归并排序;
  • 将两个排序好的子序列合并成一个最终的排序序列。

5.2动图演示

img

5.3算法分析

归并排序是一种稳定的排序方法。和选择排序一样,归并排序的性能不受输入数据的影响,但表现比选择排序好的多,因为始终都是O(nlogn)的时间复杂度。代价是需要额外的内存空间。

6、快速排序(QuickSort)

快速排序的基本思想:通过一趟排序将待排记录分隔成独立的两部分,其中一部分记录的关键字均比另一部分的关键字小,则可分别对这两部分记录继续进行排序,以达到整个序列有序。

6.1算法描述

快速排序使用分治法来把一个串(list)分为两个子串(sub-lists)。具体算法描述如下:

  • 从数列中挑出一个元素,称为"基准”(pivot);
  • 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
  • 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。

6.2动图演示

img

7、堆排序(HeapSort)

堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子节点的键值或索引总是小于(或者大于)它的父节点。

7.1算法描述

  • 将初始待排序关键字序列(R1,R2….Rn)构建成大顶堆,此堆为初始的无序区;
  • 将堆顶元素R[1]与最后一个元素R[n]交换,此时得到新的无序区(R1,R2,……Rn-1)和新的有序区(Rn),且满足R[1,2…n-1]<=R[n];
  • 由于交换后新的堆顶R[1]可能违反堆的性质,因此需要对当前无序区(R1,R2,……Rn-1)调整为新堆,然后再次将R[1]与无序区最后一个元素交换,得到新的无序区(R1,R2….Rn-2)和新的有序区(Rn-1,Rn)。不断重复此过程直到有序区的元素个数为n-1,则整个排序过程完成。

7.2动图演示

img

8、计数排序(CountingSort)

计数排序不是基于比较的排序算法,其核心在于将输入的数据值转化为键存储在额外开辟的数组空间中。作为一种线性时间复杂度的排序,计数排序要求输入的数据必须是有确定范围的整数。

8.1算法描述

  • 找出待排序的数组中最大和最小的元素;
  • 统计数组中每个值为i的元素出现的次数,存入数组C的第i项;
  • 对所有的计数累加(从C中的第一个元素开始,每一项和前一项相加);
  • 反向填充目标数组:将每个元素i放在新数组的第C(i)项,每放一个元素就将C(i)减去1。

8.2动图演示

img

8.3算法分析

计数排序是一个稳定的排序算法。当输入的元素是n个0到k之间的整数时,时间复杂度是O(n+k),空间复杂度也是O(n+k),其排序速度快于任何比较排序算法。当k不是很大并且序列比较集中时,计数排序是一个很有效的排序算法。

9、桶排序(BucketSort)

桶排序是计数排序的升级版。它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。桶排序(Bucketsort)的工作的原理:假设输入数据服从均匀分布,将数据分到有限数量的桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排)。

9.1算法描述

  • 设置一个定量的数组当作空桶;
  • 遍历输入数据,并且把数据一个一个放到对应的桶里去;
  • 对每个不是空的桶进行排序;
  • 从不是空的桶里把排好序的数据拼接起来。

9.2图片演示

img

9.3算法分析

桶排序最好情况下使用线性时间O(n),桶排序的时间复杂度,取决与对各个桶之间数据进行排序的时间复杂度,因为其它部分的时间复杂度都为O(n)。很显然,桶划分的越小,各个桶之间的数据越少,排序所用的时间也会越少。但相应的空间消耗就会增大。

10、基数排序(RadixSort)

基数排序是按照低位先排序,然后收集;再按照高位排序,然后再收集;依次类推,直到最高位。有时候有些属性是有优先级顺序的,先按低优先级排序,再按高优先级排序。最后的次序就是高优先级高的在前,高优先级相同的低优先级高的在前。

10.1算法描述

  • 取得数组中的最大数,并取得位数;
  • arr为原始数组,从最低位开始取每个位组成radix数组;
  • 对radix进行计数排序(利用计数排序适用于小范围数的特点);

10.2动图演示

img

10.3算法分析

基数排序基于分别排序,分别收集,所以是稳定的。但基数排序的性能比桶排序要略差,每一次关键字的桶分配都需要O(n)的时间复杂度,而且分配之后得到新的关键字序列又需要O(n)的时间复杂度。假如待排数据可以分为d个关键字,则基数排序的时间复杂度将是O(d*2n),当然d要远远小于n,因此基本上还是线性级别的。

基数排序的空间复杂度为O(n+k),其中k为桶的数量。一般来说n>>k,因此额外空间需要大概n个左右。

参考资料

数据结构与算法

十大经典排序算法(动图演示)

算法复杂度分析,这次真懂了-知乎(zhihu.com)

图解数据结构:树和森林与二叉树的相互转换

数据结构学习笔记之树和森林的存储结构与相关应用

【纯干货】三分钟教会你遍历二叉树!学不会举报我!!

根据先序和中序推出后序并画出线索二叉树

数据结构2小时期末速成考点总结

数据结构:树(Tree)详解

一秒学会 平衡二叉树的调整,非标题党!不简单你打我! (考研数据结构)

彻底搞懂红黑树

红黑树插入

数据结构性能挑战赛,分别进行1000万次增查删,数组、链表、红黑树参赛。

3分钟学会跳表

多线程跳表大战红黑树

数据结构——五分钟搞定哈夫曼树,会求WPL值,不会你打我

图详解

posted @ 2023-12-22 19:04  NPC老郑  阅读(55)  评论(0编辑  收藏  举报