赫夫曼树之代码实现
最后更新于:2022-04-01 15:58:28
定义它的存储结构,典型的二叉树,只是多了一个权值。
~~~
typedef struct
{
int weight;
int parent, lchild, rchild;
}HTNode, *HuffmanTree;
typedef char **HuffmanCode;
~~~
HuffmanCode是一个字符串数组,用来保存叶子结点最终的编码结果。
存储空间是多少呢? 从前一章讲的构造赫夫曼树的过程可以找出规律,n个叶子结点所构造的赫夫曼树共有2n-1个结点,这就是我们要分配的空间。
~~~
int m = 2 * n - 1;
*HT = (HuffmanTree)malloc((m+1)*sizeof(HTNode));//多分配一个空间,0号不用
~~~
函数的接口形式如下:
~~~
bool HuffmanCoding(HuffmanTree *HT, HuffmanCode *HC, int *w, int n)
~~~
HT是最终构造的赫夫曼树,是个输出参数,HC也是个输出函数,就是个字符串数组,输出最终的编码。W存放n个叶子结点的权值,当然都是大于0的整数,n是叶子结点的个数, 最后这两个都是输入参数。
构造赫夫曼树的过程代码其实很简单:
~~~
//构建赫夫曼树
for (i = n + 1; i <= m; i++)
{
if (!Select(*HT, i - 1, &s1, &s2)) return false;
(*HT)[s1].parent = i;
(*HT)[s2].parent = i;
(*HT)[i].lchild = s1;
(*HT)[i].rchild = s2;
(*HT)[i].weight = (*HT)[s1].weight + (*HT)[s2].weight;
}
~~~
select函数从HT[1...nEnd]中选出parent为0, 并且weight最小的两个结点,序号分别由s1 和 s2返回,它的实现如下:
~~~
static bool Select(HuffmanTree HT, int nEnd, int *s1, int *s2)
{
int i = 0;
int nComp = 0;
nComp = MAX;
*s1 = MAX;
*s2 = MAX;
//第一轮循环,找到最小的给s1
for (i = 1; i <= nEnd; i++)
{
if ((HT[i].weight < nComp) && (HT[i].parent == 0))
{
nComp = HT[i].weight;
*s1 = i;
}
}
//第二轮循环,找到次小的给s2
nComp = MAX;
for (i = 1; i <= nEnd; i++)
{
if ((HT[i].weight < nComp) && (i != *s1) && (HT[i].parent == 0))
{
nComp = HT[i].weight;
*s2 = i;
}
}
if ((*s1 == MAX) || (*s2 == MAX))
{
return false;
}
return true;
}
~~~
编码的结果是保存在HC中的,为了方便采用逆向保存的形式,即从叶子到根求编码,然后输出时就是正向的了。
~~~
for (i = 1; i <= n; i++)
{
start = n - 1;
for (c = i, f = (*HT)[i].parent; f != 0; c = f, f = (*HT)[f].parent)
{
//左0右1
if ((*HT)[f].lchild == c)
{
cd[--start] = '0';
}
else
{
cd[--start] = '1';
}
}
(*HC)[i] = (char *)malloc((n - start));
strcpy((*HC)[i], &cd[start]);
}
~~~
代码下载地址:
http://download.csdn.net/detail/pony_maggie/8209175
或
https://github.com/pony-maggie/HuffmanCode
赫夫曼树之理论概述
最后更新于:2022-04-01 15:58:26
转载请注明出处
http://blog.csdn.net/pony_maggie/article/details/41594015
作者:小马
**一什么是赫夫曼树**
赫夫曼树是指带权路径最短的树,从根结点到叶子结点所经过的结点数(不包括根结点,包括叶子结点)叫路径,如果给叶子结点赋予权值,那么路径和权值的乘积就是访问该叶子结点的代价,对于一棵树来讲,使访问所有的叶子结点的代价最小的树,就是赫夫曼树。
比如下面三个图:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-13_575e929590dff.jpg)
可以计算它们的带权路径长度,分别为
(a) 图是36
(b) 图是46
(c) 图是 35
所以c图是赫夫曼树。
**二如何构造赫夫曼树**
构造一棵赫夫曼树的步骤其实不复杂,简单来讲就是权值大的尽量靠近根结点,而且是越大的越靠近。这样得出的效果是权值越大的结点,可以经过相对较少的距离到达,从而使程序的效率提高。这里的所说的效率,即包括时间上也包括空间上,后面我会讲到两个应用例子,分别就是一个时间上的优化,一个空间上的优化。
赫夫曼本人给了一个基本的算法,如下:
(1) 将w1、w2、…,wn看成是有n 棵树的集合F(每棵树仅有一个根结点);
(2) 在这些树中选出两个根结点的权值最小的树合并,作为一棵新树的左、右子树,且新树的根结点权值为其左、右子树根结点权值之和;
(3)从F中删除选取的两棵树,并将新树加入F
(4)重复(2)、(3)步,直到森林中只剩一棵树为止,该树即为所求得的哈夫曼树
**三应用举例**
了解的具体的算法之后,必须要知道它的应用场景,不然也就只能停留在理论阶段了。这里给出两个应用的例子。
比如统计一次考试中的学生成绩,划分为5个等级,60分以下为不及格,60到70之间是及格,70到80之间是中等,80到90之间是良好,90到100之间是优秀。本次考试各个阶段学生所占比例如下:
| 分数 | 0-59 | 60-69 | 70-79 | 80-89 | 90-100 |
|-----|-----|-----|-----|-----|-----|
| 比例 | 0.05 | 0.15 | 0.40 | 0.30 | 0.10 |
假设有10000个学生,然后我们用下面的代码来实现统计:
~~~
if (a < 60) b = "不及格";
else if (a < 70) b = "及格";
else if (a < 80) b = "中等";
else if (a < 90) b = "良好";
else b = "优秀";
~~~
代码对应的树结构如下:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-13_575e9295a54b9.jpg)
这样一共需要比较30000多次。上面的代码不管你的分数是多少,总要从小于60开始比较,比如一个学生的成绩是85,他要被比较四次才能有结果,不幸的是, 大部分学生的成绩都是在70到90之间,都要经过至少三次以上的比较才能完成。
如果我们能把这个占大多数人的分数区间放在前面,不就可以大大减少比较的次数了吗?先按照前面章节讲的步骤构造一棵赫夫曼树,如下图:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-13_575e9295bf2ce.jpg)
对应的代码是:
~~~
if (a >= 70 && a < 80) b = "中等";
else if (a >= 80 && a < 90) b = "良好";
else if (a >= 60 && a < 70) b = "及格";
else if (a < 60) b = "不及格";
else b = "优秀";
~~~
这样比较的次数变为22000次,大大提高效率。
再来一个应用的例子。
电报传送时,一般要把字符转成二进制的编码,比如一串字符, “ABACCDA”, 四种字符,可以用二位表示一种,比如A是00, B是01, C是10, D是11,那么电报发送的就是00010010101100,对方收到电报时可以两位一组解出来。
上面的方法没有什么问题,但是一般传送电报肯定希望能用最短的长度传递尽量多的信息,是不是还可以优化呢。当然,我们用不同长度的编码来表示这些字符,出现次数多的字符尽可能的短,出现次数少的可以偏长一些,这样可以构造出一个比上面更短的电报编码。
上面的字符信息,A和C现的次数较多,分别为两次和三次,B和D都是一次。按照上面的方法来构造一棵赫夫曼树,如下:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-13_575e9295d47e5.jpg)
规则是左0右1, 这样,A是0, B是110, C是10, D是111, 最后的编码是0110010101110, 一共是13个bit位,原来是14个bit位,确实变短了。
再来看看电报接收的一方收到这串编码能不能解出来,试一下发现可以正常解析,不会产生歧义。这是因为任意一个字符的编码都不是另一个字符的编码前缀。这也是二叉树本身自带的一个功能,我们通过这种左0右1的方式得到的编码就可以达到这种效果。
下篇讲如何用代码实现赫夫曼编码
关于二叉树的几种遍历方法
最后更新于:2022-04-01 15:58:24
转载请注明出处
http://blog.csdn.net/pony_maggie/article/details/38390513
作者:小马
一 二叉树的一些概念
二叉树就是每个结点最多有两个子树的树形存储结构。先上图,方便后面分析。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-13_575e929540476.jpg)
**1 满二叉树和完全二叉树**
上图就是典型的二叉树,其中左边的图还叫做满二叉树,右边是完全二叉树。然后我们可以得出结论,满二叉树一定是完全二叉树,但是反过来就不一定。满二叉树的定义是除了叶子结点,其它结点左右孩子都有,深度为k的满二叉树,结点数就是2的k次方减1。完全二叉树是每个结点都与深度为k的满二叉树中编号从1到n一一对应。
**2 树的深度**
树的最大层次就是深度,比如上图,深度是4。很容易得出,深度为k的树,拥有的最大结点数是2的k次方减1。
**3 树的孩子,兄弟,双亲**
上图中,B,C是A的孩子,B,C之间互为兄弟,A是B,C的双亲。
二如何创建二叉树
先说说二叉树的存储结构,跟很多其它模型一样,也有顺序和链式两种方式。前者虽然使用简单,但是存在浪费空间的问题,举个例子,下图的二叉树,用顺序的方式存储(0表示空,没有子树)是:
1 2 3 4 5 6 7 0 0 0 0 8 0 0 0
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-13_575e92955825f.jpg)
是不是相当浪费空间呢。
链式结构可以定义如下:
~~~
typedef struct _BiTNode
{
int data;
_BiTNode *leftChild;
_BiTNode *rightChild;
}BiTNode, *pBiTree;
~~~
然后就可以写一个函数来创建二叉树,过程是在控制台输入a表示退出当前这一层,不再为该层创建左右孩子。输入其它字母表示继续创建。比如下面的输入序列:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-13_575e92956a6e8.jpg)
创建了如下结构的二叉树,
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-13_575e92957cea9.jpg)
每个结点里的数值是随机生成的小于100的数字。同时我也写了一个自动的命令序列函数,方便测试,不用手动输入,非自动和自动创建的函数如下:
~~~
//创建二叉树, 先序顺序
int CreateBiTree(pBiTree *root)
{
char ch = 0;
fflush(stdin);
if ((ch = getchar()) == 'a')//控制树的结构
{
*root = NULL;
}
else
{
*root = (BiTNode *)malloc(sizeof(BiTNode));
if (!(*root))
{
return RET_ERROR;
}
(*root)->data = GetRandom();
CreateBiTree(&(*root)->leftChild);
CreateBiTree(&(*root)->rightChild);
}
return RET_OK;
}
int g_i = 0;
//创建二叉树,自动执行,方便测试
int CreateBiTreeAuto(pBiTree *root)
{
char szOrder[] = "bbaabaa";
char ch = 0;
if (szOrder[g_i++] == 'a')//控制树的结构
{
*root = NULL;
}
else
{
*root = (BiTNode *)malloc(sizeof(BiTNode));
if (!(*root))
{
return RET_ERROR;
}
(*root)->data = GetRandom();
CreateBiTreeAuto(&(*root)->leftChild);
CreateBiTreeAuto(&(*root)->rightChild);
}
return RET_OK;
}
~~~
三遍历顺序
**先序遍历**
先序遍历是先访问根结点,再左子树,再右子树,比如图1中的右图,先序遍历的输出如下:
A,B,D,H,I,E,J,K,C,F,G
根据上面的思想,很容易用递归的形式写出先序遍历的代码:
~~~
//先序遍历
int PreOrderVisitTree(pBiTree T, VisitType pFuncVisit)
{
if (T)
{
(*pFuncVisit)(T->data);
if (PreOrderVisitTree(T->leftChild, pFuncVisit) == RET_OK)
{
if (PreOrderVisitTree(T->rightChild, pFuncVisit) == RET_OK)
{
return RET_OK;
}
}
return RET_ERROR;
}
else
{
return RET_OK;
}
}
~~~
**中序遍历和后序遍历**
有了先序的经验,这两个就很好理解了,中序是先访问左子树, 再根结点,再右子树, 后序是先访问左子树, 再右子树,再根结点。代码更容易,只要改一下调用顺序就可以了。
不过我这里给出一种非递归的实现。递归固然是清晰明了,但是存在效率低的问题,非递归的方案用栈结构来存结点信息,通过出栈访问来遍历二叉树。它思想是这样的,当栈顶中的指针非空时,遍历左子树,也就是左子树根的指针进栈。当栈顶指针为空时,应退至上一层,如果是从左子树返回的,访问当前层,也就是栈顶中的根指针结点。如果是从右子树返回,说明当前层遍历完毕,继续退栈。代码如下:
~~~
//中序遍历, 非递归实现
int InOrderVisitTree(pBiTree T, VisitType pFuncVisit)
{
ponyStack binaryTreeStack;
InitStack(&binaryTreeStack, 4);
Push(&binaryTreeStack, &T);
pBiTree pTempNode;
while (!IsEmptyStack(binaryTreeStack))
{
while((GetTop(binaryTreeStack, &pTempNode) == RET_OK) && (pTempNode != NULL))
{
Push(&binaryTreeStack, &(pTempNode->leftChild));
}
Pop(&binaryTreeStack, &pTempNode);
if (!IsEmptyStack(binaryTreeStack))
{
Pop(&binaryTreeStack, &pTempNode);
(*pFuncVisit)(pTempNode->data);
Push(&binaryTreeStack, &(pTempNode->rightChild));
}
}
return RET_OK;
}
~~~
代码下载地址:
http://download.csdn.net/detail/pony_maggie/7714493
或
https://github.com/pony-maggie/BinaryTreeDemo
KMP算法模式匹配
最后更新于:2022-04-01 15:58:21
转载请注明出处
http://blog.csdn.net/pony_maggie/article/details/37832707
作者:小马
在一个长串中查找一个子串是较常用的操作。各种信息检索系统,文字处理系统都少不了。本文介绍一个非常著名的KMP模式匹配算法用于子串查找。
先抛开KMP,正常情况一下我们会如何设计这个逻辑。一个主串S, 要在里面查找一个子串T,如果找到了返回T在S中开始的位置,否则返回-1。应该不难,需要一个辅助函数,从一个串口的指定位置,取出指定长度的子串。思想是这样:
在主串S中从开始位置,取长度和T相等的子串比罗,若相等,返回该位置的索引值,否则位置增加1, 继续上面的过程。代码很简单,如下:
~~~
//普通方法查找子串
//在主串s中查找t, 若找到匹配,返回索引位置(从0到len-1)
//否则返回-1
int IndexOfString(char* srcString, char* keyString)
{
int nSrcLen = 0;
int nKeyString = 0;
int i = 0;
char szTemp[1024] = {0};//假设足够大
if ((srcString == NULL) || (keyString == NULL))
{
return -1;
}
nSrcLen = strlen(srcString);
nKeyString = strlen(keyString);
if (nKeyString > nSrcLen)
{
return -1;
}
while(i < (nSrcLen-nKeyString))
{
memset(szTemp, 0x00, sizeof(szTemp));
SubString(srcString, szTemp, i, nKeyString);
if (memcmp(szTemp, keyString, nKeyString) != 0)
{
i++;
}
else return i;
}
return -1;
}
~~~
再进一步,把辅助函数去掉,通过频繁操作下标也同样可以实现,思想跟上面查不多,只不过换成一个个字符比较,代码如下:
~~~
int IndexOfString1(char* srcString, char* keyString)
{
int nSrcLen = 0;
int nKeyLen = 0;
int i = 0;
int j = 0;
char szTemp[1024] = {0};//假设足够大
if ((srcString == NULL) || (keyString == NULL))
{
return -1;
}
nSrcLen = strlen(srcString);
nKeyLen = strlen(keyString);
if (nKeyLen > nSrcLen)
{
return -1;
}
while((i < nSrcLen) && (j <nKeyLen))
{
if (srcString[i] == keyString[j])
{
i++;
j++;
}
else
{
i = i - j + 1;//主串回退到下一个位置
j = 0; //子串重新开始
}
}
if (j >= nKeyLen)
{
return (i - nKeyLen);//找到
}
return -1;
}
~~~
分析一下上面算法的时间复杂度(两个算法其实是一样的)。举个例子,主串是:
“A STRING SEARCHING EXAMPLE CONSISTINGOF SIMPLE TEXT”
子串是
“STING”
用上面的算法,会发现除了上面标记红色的字符比较了两次,其它都是比较一次,如果主串的长度是m, 子串的长度是n, 时间复杂度基本是O(m+n)。好像不错,效率挺高。再来看一个。主串是:
“000000000000000000000000000000000000000000000000000000000001”
子串是
“00000001”
在脑海里想像一下这个过程,很容易得出它的时间复杂度是O(m*n)。所以这种类似穷举的查找子串算法,时间复杂度与主串和子串的内容有很大关系,复杂度不固定。而且像上面那样的01串在计算机处理中还比较常见,所以需要更好的算法。
KMP(三个发明人的名字首字母)算法可以保证不论哪种情况都可以在O(m+n)的时间复杂度内完成匹配。它改进的地方是当出现比较不等时,主串中位置不回退,而是利用已经匹配的结果,将子串向右滑动尽可能远的距离后,再继续比较,看个例子:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-13_575e9295265c7.jpg)
在第三趟匹配中,当i=12, j=7时,字符不相等,这时不用回退到i=7,j=1重新比较,而是i不变,j变成next[j]的值就行了,上图中是3, 也就是图中第四趟的比较位置。
这个确实很强大,现在要解决的问题是next[j]如何计算。其实这个推导也不难,很多算法的书上都有详细的过程,我这里就不赘述了。只把结果给出来:
1 当j = 0时,next[j] =-1。
2 当j =1时,next[j] = 0。
3 满足1 < k < j,且p[0…k-1] =p[j-k…j-1]的最大的k, next[j] = k。
4 其它情况,next[j] = 0。
根据上面的逻辑,很容易写出一个计算next的函数,如下:
~~~
static void getNext(char* strSub)
{
int j, k, temp;
int nLen = 0;
j = k = temp = 0;
nLen = strlen(strSub);
memset(g_next, 0x00, sizeof(g_next));//每次进来都清空
for (j = 0; j < nLen; j++)
{
if (j == 0)
{
g_next[j] = -1;
}
else if (j == 1)
{
g_next[j] = 0;
}
else //取集合不为空时的最大值
{
temp = j - 1;
for (k = temp; k > 0; k--)
{
if (isEqual(strSub, j, k))
{
g_next[j] = k;
break;
}
}
if (k == 0)//集合为空
{
g_next[j] = 0;
}
}
}
}
~~~
得到next之后,整个过程如下: 以指针i和j分别表示主串和子串中正在比较的字符,若Si = Pj, 则i和j都增1,继续下一个字符的比较。否则i不变,j退到next[j]的位置再比较,若相等,再各自增1,否则j再退到next[j]。循环进行,如果中间遇到next[j]为-1的情况,此时主串要增1,子串j变为0,表示要从主串的下一个位置重新开始比较。
把上面的过程转化为代码:
~~~
//KMP算法查找子串
//在主串s中查找t, 若找到匹配,返回索引位置(从0到len-1)
//否则返回-1
int IndexOfStringKMP(char* srcString, char* keyString)
{
int i = 0;
int j = 0;
int nSrcLen = 0;
int nKeyLen = 0;
if ((srcString == NULL) || (keyString == NULL))
{
return -1;
}
nSrcLen = strlen(srcString);
nKeyLen = strlen(keyString);
if (nKeyLen > nSrcLen)
{
return -1;
}
getNext(keyString);//先计算next值
while ((i < nSrcLen) && (j < nKeyLen))
{
if ((srcString[i] == keyString[j]) || (j == -1))
{
i++;
j++;
}
else
{
j = g_next[j];
}
}
if (j >= nKeyLen)
{
return (i - nKeyLen);//找到
}
return -1;
}
~~~
代码下载地址:
http://download.csdn.net/detail/pony_maggie/7630329
或
https://github.com/pony-maggie/StringIndex
集合划分问题
最后更新于:2022-04-01 15:58:19
转载请注明出处
http://blog.csdn.net/pony_maggie/article/details/37729745
作者:小马
思考下面一个问题,
给定正整数n和m,计算出n个元素的集合可以划分为多少个不同的由m(m<=n)个不同的非空子集组成的集合。
这类题可以叫做集合划分问题。面试题里经常出现。先来考虑一个问题,这个算法有实际的应用吗?可能用在类似资源分配的例子上,比如有n个资源,m个工程,把这些资源分给不同的工程,需要求出不同的分法,然后计算不同的分法将获得的利润(这个例子似乎有点牵强,谁有更好的请给我留言,谢谢)。
解题的思路,一种是分治的思想。把n个元素编号,对於最后那个n号元素,有两种情况:
一种是独立组成一个集合,另一种是和别的元素混在一起。
对於第一种情况,等价于把前n-1个元素分成m-1份,然后n号元素单独放。
对於第二种情况,等价于把前n-1个元素分成m份,然后把n号元素放入这m个集合中的一个(有m种放法)
总数就是
F(n,m) =F(n-1,m-1) + m * F(n-1,m)
先想几种极端的情况,
m=n的情况,结果很明显是1。
m=1的情况,结果也是1。
n=1的情况,m只能等于1, 结果也是1。
有了上面的思想,基本上基于递归的代码就可以出来了,如下:
~~~
//基于分治的思想,递归实现
int calculateSet(int n ,int m)
{
if ((m > n) ||(m == 0))
{
return 0;
}
else if ((m == 1)||(n==1)||(m == n))
{
return 1;
}
else
{
return (calculateSet(n-1, m-1) + m*calculateSet(n-1, m));
}
}
int _tmain(int argc, _TCHAR* argv[])
{
int n1 = 4;
int m1= 2;
printf("%d\n", calculateSet(n1,m1));
return 0;
}
~~~
关于符号位扩展你又知道多少
最后更新于:2022-04-01 15:58:17
转载请注明出处
http://blog.csdn.net/pony_maggie/article/details/37535577
作者:小马
先看两段代码, 一个是C,一个是java。
~~~
int _tmain(int argc, _TCHAR* argv[])
{
char b = 0x83;
short s1 = (short)b;
short s2 = (short)(b&0xff);
printf("s1 = %d\n", s1);
printf("s2 = %d\n", s2);
return 0;
}
~~~
~~~
public static void main(String[] args)
{
byte b = (byte)0x83;
short s1 = (short)b;
short s2 = (short)(b&0xff);
System.out.printf("s1 = %d\n", s1);
System.out.printf("s2 = %d\n", s2);
}
~~~
运行结果其实是一样的:
s1 = -125
s2 = 131
用两种语言,是想说明它的通用性,表示这个特性跟语言本身无关。那么原因是什么呢?
首先,第二个结果才是我们期望的,这个应该都同意(至少大部分情况下都是这样)。其次如果变量b是正数(这里是负数,因为char表示有符号数,0x83最高位是1,表示负数),S1和S2的结果是相等的。
所以,问题的核心其实还是变量b的这个符号位。计算机里从低精度数向高精度数转换时,比如这里从char到short, 肯定会在前面扩展一些bit位,从而达到高精度数的长度。那么扩展时,是补0还是补1呢?这里有个原则就是,有符号数扩展符号位,也就是1,无符号数扩展0。
再看看上面的代码,s1其实是0xff83, 是个负数,它表示的值就是-125(注意0xff83是补码表示,计算原值要换成原码)。而s2是0x0083, 因为它强制与上了0xff,其实就是与上了0x00ff。这样就把高字节转成了0x00,消除了符号位。
为了证明上面的理论,可以做一个实验,把代码改成这样:
~~~
int _tmain(int argc, _TCHAR* argv[])
{
char b = 0x83;
short s1 = (short)b;
short s2 = (short)(b&0xffff);
printf("s1 = %d\n", s1);
printf("s2 = %d\n", s2);
return 0;
}
~~~
运行,结果是s1=s2=-125。这也支持了上面的理论分析。
趁热打铁,再看一个示例:
~~~
int _tmain(int argc, _TCHAR* argv[])
{
char a = 0xff;
if (a == 0xff)
{
printf("equal\n");
}
return 0;
}
~~~
运行的结果是, “equal”并没有打印出来。你可能已经分析出原因了。字符a在和0xff比较时,被隐式的转换成int(因为0xff是整型),然后a做了符号位扩展,变为0xffffffff, 这个值和0x000000ff比较,当然是不等的。
我希望已经讲清楚了,欢迎拍砖。
若干排序算法简单汇总(二)
最后更新于:2022-04-01 15:58:14
转载请注明出处
http://blog.csdn.net/pony_maggie/article/details/36706131
作者:小马
一希尔排序
上一篇讲到的直接插入排序,时间复杂度O(n^2). 请在脑海里想一下它的过程。如果一个序列本来就是有序的,对它排序的时间复杂度是O(n)。所以当序列基本有序时,插入排序排序的效率大大提高,因为减少了移动的动作。
另外,直接插入排序还有一个特点,当n比较小时,它的效率比较高。
希尔排序正是基于上面两个思想做的一种改进算法。它先将整个序列分成若干个小序列,对每个小序列做直接插入排序,这样整个序列变得“基本有序”,然后对整个序列做一次直接插入排序,得到最终结果。不过希尔排序并不是简单地逐段分割,而用相隔某个增量的记录组成一个序列。如下图所示:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-13_575e9294bbc3f.jpg)
一开始增量为3, 有三组,{9,8,4}, {1, 3, 6},{5,7,2},分别直接插入排序得到2图,然后增加变为2,继续上面的过程,最后当增量为1时,数组就有序了。三趟排序用的增量构造一个增量序列{3,2,1}。这个不是固定的,但是一个好的增量序列,应该没有除1以外的公因子,并且最后一个增量必须等于1。思路很清晰了,上代码吧。
~~~
//一趟插入排序, dk是单次的增量
static void shellInsert(int nArray[], int nLength, int dk)
{
int i = 0;
int j = 0;
int nSerity = 0;
for (i = dk; i < nLength; i++)
{
if (nArray[i] < nArray[i-dk])
{
nSerity = nArray[i];
for (j = i-dk; (j >= 0)&&(nSerity < nArray[j]); j-=dk)
{
nArray[j+dk] = nArray[j];
}
nArray[j+dk] = nSerity;
}
}
}
int shellSort(int nArray[], int nLength)
{
int k = 0;
int dkArray[] = {3, 2, 1}; //默认使用的增量序列
int dkLength = 3;
for (k = 0; k < dkLength; k++)
{
shellInsert(nArray, nLength, dkArray[k]);
}
return 0;
}
~~~
它的复杂度计算涉及到一些数学难题,你只要知道它的效率比较直接插入排序要高一些就行了。
二快速排序
有些地方会提到快速排序是对冒泡排序的一种改进,我倒是觉得不要这么联想,会误导你学习快速排序。
快速排序思想先选取一个“枢纽元素”,一般就是序列的第一个元素。把比这个枢纽小的数据放一边,比它大的放另一边。这样一趟之后序列变为, 一部分中的所有元素都比另一部分中的所有元素小,但是部分之间的记录可能是无序的。然后对每一部分再用样的思想继续分,最后就变为有序序列。如下图所示:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-13_575e9294e0595.jpg)
通过上面的步骤,自然想到用递归来实现快速排序,没错,上代码。
~~~
static int partition(int nArray[], int nLow, int nHigh)
{
int nPivot = nArray[nLow];
while (nLow < nHigh)
{
while ((nLow < nHigh) && (nArray[nHigh] >= nPivot)) nHigh--;
nArray[nLow] = nArray[nHigh];
while ((nLow < nHigh) && (nArray[nLow] <= nPivot)) nLow++;
nArray[nHigh] = nArray[nLow];
}
nArray[nLow] = nPivot;
return nLow;
}
static void sortProcess(int nArray[], int nLow, int nHigh)
{
int nPartition = 0;
if (nLow < nHigh)
{
nPartition = partition(nArray, nLow, nHigh);
sortProcess(nArray, nLow, nPartition-1);
sortProcess(nArray, nPartition+1, nHigh);
}
}
int quickSort(int nArray[], int nLength)
{
sortProcess(nArray, 0, nLength-1);
return 0;
}
~~~
快速排序时间复杂度是O(nlogn),是目前公认的效率比较高的排序算法。
三归并排序
归并排序算是一种比较特殊的排序算法,它将两个有序表(长度分别是m,n)合并成另一个有序表,这个动作可在O(m+n)的时间复杂度实现。对于个有n个元素的无序序列,可以看成n个有序的子序列,然后两两归并,得到一个n/2长度为2或1(想想为什么有1)的有序子序列,继续两两归并,直接得到一个长度为n的有序序列为止。如下图所示:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-13_575e9295091c4.jpg)
这里我们用递归的方法来实现,好理解一些,非递归的方法稍复杂一些。代码如下:
~~~
//将有序的srcArray[i..m]和srcArray[m+1..n],归并到destArray[i..n]
static void Merge(int srcArray[], int destArray[], int i, int m, int n)
{
int j = 0;
int k = 0;
for (j = m+1,k=i; (i<=m)&&(j<=n); ++k)
{
if (srcArray[i] < srcArray[j])
{
destArray[k] = srcArray[i++];
}
else
{
destArray[k] = srcArray[j++];
}
}
//剩下的直接拷过来
while (i <= m)
{
destArray[k++] = srcArray[i++];
}
while (j <= n)
{
destArray[k++] = srcArray[j++];
}
}
static void MSort(int srcArray[], int destArray[], int s,int t)
{
int m = 0;
int destArray2[256] = {0}; //辅助数组,空间根据实际情况分配.
if (s == t)
{
destArray[s] = srcArray[s];
}
else
{
m = (s + t)/2;
MSort(srcArray, destArray2, s, m);
MSort(srcArray, destArray2, m+1, t);
Merge(destArray2, destArray, s, m, t);
}
}
//递归方法实现归并排序
int MergeSort(int nArray[], int nLength)
{
int nDestArray[256] = {0};
int i = 0;
MSort(nArray, nDestArray, 0, nLength-1);
while (i<nLength)nArray[i] = nDestArray[i++];
return 0;
}
~~~
它的时间复杂度是O(nlog2n)。
代码下载地址:
http://download.csdn.net/detail/pony_maggie/7568971
或
https://github.com/pony-maggie/SortDemo
若干排序算法简单汇总(一)
最后更新于:2022-04-01 15:58:12
转载请注明出处
http://blog.csdn.net/pony_maggie/article/details/35819279
作者:小马
从题目看,首先不是全部是若干。排序算法很多,我个人的能力也有限,不可能都讲到。另外,是简单汇总,是希望能用最简单的代码,最简短的语言说明问题,不搞太多理论分析。
就像前面说的,排序算法有很多,而且不存在哪一种最不好,哪一种最好这样的说法。根据用途不同选择最适合的就行了。不过仅从时间复杂度来看,基本上有两种,一种是O(n^2), 一种是O(nlogn)。
所谓的时间复杂度,其实是基于多少次基本操作定义的,在排序算法中,基本操作指两类,一是比较,二是记录从一个位置移动另一个位置。下面讲到的排序算法都会涉及到这些操作。
一直接插入排序
先从一个最简单的切入。它的思种是这样的,把一个数插入到已排好的序列中。比如已有一个有序序列:
{ 12, 23, 25, 40}
现在有一个数18要插入进来,并且保证插入后序列还是有序。18开始和序列中的所有数逐一比较(从右向左比较),比40小,40后移,比25小,25后移,比23小,23后移,跟12比,发现比12大,停在这里,插入到23的位置,最终变为:
{ 12, 18, 23, 25, 40}
上面的过程叫一趟插入排序,基于这个思想, 我们可以认为数组第一个元素是有序的,所以可以从第二个元素开始,每个元素都做一趟插入排序就可以得到一个有序序列。代码就很简单了,
~~~
int insertSort(int nArray[], int nLength)
{
int i = 0;
int j = 0;
int nSerity = 0;//备份要插入的那个元素
for (i = 1; i < nLength; i++)
{
if (nArray[i] < nArray[i-1])
{
nSerity = nArray[i];
nArray[i] = nArray[i-1];
for (j = i-2; (j >= 0)&&(nSerity < nArray[j]); j--)
{
nArray[j+1] = nArray[j];
}
nArray[j+1] = nSerity;
}
}
return 0;
}
~~~
很容易看出它的时间复杂度是O(n^2)
二冒泡排序
这个排序算法基本是大学老师必讲的,因为它除了简单之外,也确实比较好玩。思路是这样的,一个无序序列a[n], a[1]和a[2]比较,如果a[1]>a[2], 它们就交换位置,否则不做处理。继续a[2]和a[3]同样的原理比较,一直到a[n-1]和a[n]比较。
上面的过程叫一趟冒泡,请你在脑海里想像下这个过程,我下面要说的这个结论希望你能想明白,那就是经过一趟冒泡后,序列中最大的那个元素已经被换到a[n]的位置了。有没有觉得这个过程就像冒泡一样,只不过这个泡是向下冒的。
做第二趟冒泡时,只要对a[1]~a[n-1]操作就行了,结果是序列中第二大的那个元素在a[n-1]的位置了。然后经过n趟冒泡后,排序就完成了。
通过上面的过程也很容易得出冒泡排序的时间复杂度是O(n^2),可以上代码了,
~~~
//bChange作用是为了对于已排好序的序列,能
//及时退出来。
int bubbleSort(int nArray[], int nLength)
{
int i = 0;
int j = 0;
int nTemp = 0;
bool bChange = true;
for (i = 0; (i < nLength) &&(bChange); i++)
{
bChange = false;
for (j = 0; j < (nLength - i - 1); j++)
{
if (nArray[j] > nArray[j+1])
{
nTemp = nArray[j];
nArray[j] = nArray[j+1];
nArray[j+1] = nTemp;
bChange = true;
}
}
}
return 0;
}
~~~
可以好好理解一下代码中bChange变量的作用,这里不多说解释了,留给大家思考。
这篇就打算写这么多了,主要篇幅太长怕大家看着厌烦,过几天有空了再接着写吧。
代码下载地址:
http://download.csdn.net/detail/pony_maggie/7568971
或
https://github.com/pony-maggie/SortDemo
求一个集合的所有子集问题
最后更新于:2022-04-01 15:58:10
转载请注明出处
http://blog.csdn.net/pony_maggie/article/details/31042651
作者:小马
一个包含n个元素的集合,求它的所有子集。比如集合A= {1,2,3}, 它的所有子集是:
{ {1}, {2}, {3}, {1,2}, {1,3}, {2,3}, {1,2,3}, @}(@表示空集)。
这种问题一般有两种思路,先说说第一种,递归。递归肯定要基于一个归纳法的思想,这个思想用到了二叉树的遍历,如下图所示:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-13_575e9258a7ef3.jpg)
可以这样理解这张图,从集合A的每个元素自身分析,它只有两种状态,或是某个子集的元素,或是不属于任何子集,所以求子集的过程就可以看成对每个元素进行“取舍”的过程。上图中,根结点是初始状态,叶子结点是终结状态,该状态下的8个叶子结点就表示集合A的8个子集。第i层(i=1,2,3…n)表示已对前面i-1层做了取舍,所以这里可以用递归了。整个过程其实就是对二叉树的先序遍历。
根据上面的思想,首先需要一个结构来存储元素,这个”取舍”过程,其实就是在线性结构中的增加和删除操作,很自然考虑用链式的存储结构,所以我们先来实现一个链表:
~~~
typedef struct LNode
{
int data;
LNode *next;
}LinkList;
//建立一个链表,你逆向输入n个元素的值
int listCreate(LinkList *srcList, int number)
{
LinkList *pTemp;
int i = 0;
srcList->next = NULL;
srcList->data = 0;
for (i = number; i > 0; --i)
{
pTemp = (LinkList *)malloc(sizeof(LNode));
pTemp->data = i+20;//随便赋值
pTemp->next = srcList->next;
srcList->next = pTemp;
}
return 0;
}
//销毁一个链表
int listDestroy(LinkList *srcList)
{
if (!srcList || !srcList->next)
{
return 0;
}
LinkList *p1 = srcList->next;
LinkList *p2 = p1->next;
do
{
free(p1);
p1 = p2;
if (p2 != NULL)
{
p2 = p2->next;
}
}while (p1);
return 0;
}
//插入操作
//在strList第nIndex之前插入数据data
//nIndex最小为1
int listInsert(LinkList *srcList, int nIndex, int data)
{
LinkList *pStart = srcList;
int j = 0;
if (nIndex < 1)
{
return 0;
}
while((pStart) && (j < nIndex-1))
{
pStart = pStart->next;
j++;
}
if ((!pStart) || (j > nIndex-1))
{
return -1;//出错
}
LinkList *temp = (LinkList *)malloc(sizeof(LNode));
temp->data = data;
temp->next = pStart->next;
pStart->next = temp;
return 0;
}
//删除操作
//strList第nIndex位置的结点删除,并通过data返回被删的元素的值
//通常情况下返回的这个值是用不到的,不过这里也保留备用
int listDelete(LinkList *srcList, int nIndex, int *data)
{
LinkList *pStart = srcList;
int j = 0;
if (nIndex < 1)
{
return 0;
}
while((pStart) && (j < nIndex-1))
{
pStart = pStart->next;
j++;
}
if ((!pStart) || (j > nIndex-1))
{
return -1;//出错
}
LinkList *pTemp = pStart->next;
pStart->next = pTemp->next;
*data = pTemp->data;
free(pTemp);
}
~~~
有了这个链表,递归算法实现起来就很容易了:
~~~
//求冥集,nArray是存放n个元素的数组
//首次调用i传1,表示已对前面i-1个元素做了处理
void GetPowerSet(int nArray[], int nLength, int i, LinkList *outPut)
{
int k = 0;
int nTemp = 0;
if (i >= nLength)
{
printList(*outPut);
}
else
{
k = listLength(outPut);
listInsert(outPut, k+1, nArray[i]);
GetPowerSet(nArray, nLength, i+1, outPut);
listDelete(outPut, k+1, &nTemp);
GetPowerSet(nArray, nLength, i+1, outPut);
}
}
~~~
还有一种思想比较巧妙,可以叫按位对应法。如集合A={a,b,c},对于任意一个元素,在每个子集中,要么存在,要么不存在。
映射为子集:
(a,b,c)
(1,1,1)->(a,b,c)
(1,1,0)->(a,b)
(1,0,1)->(a,c)
(1,0,0)->(a)
(0,1,1)->(b,c)
(0,1,0)->(b)
(0,0,1)->(c)
(0,0,0)->@(@表示空集)
观察以上规律,与计算机中数据存储方式相似,故可以通过一个整型数与集合映射...000 ~ 111...111(表示有,表示无,反之亦可),通过该整型数逐次增可遍历获取所有的数,即获取集合的相应子集。
实现起来很容易:
~~~
void GetPowerSet2(int nArray[], int nLength)
{
int mark = 0;
int i = 0;
int nStart = 0;
int nEnd = (1 << nLength) -1;
bool bNullSet = false;
for (mark = nStart; mark <= nEnd; mark++)
{
bNullSet = true;
for (i = 0; i < nLength; i++)
{
if (((1<<i)&mark) != 0) //该位有元素输出
{
bNullSet = false;
printf("%d\t", nArray[i]);
}
}
if (bNullSet) //空集合
{
printf("@\t");
}
printf("\n");
}
}
~~~
分析代码可以得出它的复杂度是O(n*2^n)。
代码下载地址:
https://github.com/pony-maggie/PowerSetDemo
或
http://download.csdn.net/detail/pony_maggie/7499161
关于栈及其应用示例
最后更新于:2022-04-01 15:58:08
转载请注明出处
http://blog.csdn.net/pony_maggie/article/details/30802249
作者:小马
作为一种常用的数据结构, 了解栈对于算法的学习是非常必要的。栈有先进后出的特点,栈底指向数据表中的第一个元素,栈顶指向最后一个元素的下一个位置。如下图所示:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2016-06-13_575e925892318.jpg)
栈和线性表类似,也是有两种存储结构,分别为顺序结构和链式结构。大部分情况下,栈使用前者,这和它的使用场景有关,因为通常情况下我们不会对栈进行频繁地,随机地插入,删除操作。下面是我用顺序结构实现的栈,这个栈有个特点就是它的通用性,因为我并没有限制它所存储的数据类型,代码如下:
~~~
//void**其为双指针,意味入栈和出栈的将只是对应数据的地址,而不需要对数据本身进行拷贝
typedef struct
{
char *base;
char *top;
int elementSize; //元素所点字节大小
int stackSize; //当前已分配的空间(注意不是元素的实际个数)
}ponyStack;
int InitStack(ponyStack *stack, int elementSize)
{
stack->base = (char *)malloc(STACK_INIT_SIZE * sizeof(char)*elementSize);
if (!stack->base)
{
return RET_ERROR;
}
stack->top = stack->base; //为空
stack->stackSize = STACK_INIT_SIZE;
stack->elementSize = elementSize;
return RET_OK;
}
int ClearStack(ponyStack *stack)
{
stack->top = stack->base;
return RET_OK;
}
bool IsEmptyStack(ponyStack stack)
{
if (stack.top == stack.base)
{
return true;
}
return false;
}
~~~
这里没有贴出全部的代码,更完整的可以从最后的地址那里下载。注意elementSize,这个是栈可以做到通用的核心。不理解的可以再研究一下代码。
看一个栈的使用示例,数制转换。十进制转八进制。例如(1348)十进制= (2504)八进制,它基于如下的原理:
N N/8 N%8
1348 168 4
168 21 0
21 2 5
2 0 2
所以很明显,N不断的除8,每次的余数就是结果的其中一个因子,注意先出来的因子是低位的数,可以考虑用栈来保存每次取余的结果,那么出栈的顺序就是实际的结果顺序。代码很简单:
~~~
int decimalToOctonary(int decimalNumber)
{
double octNumber = 0;
int nCount = 0;
int nTemp = 0;
ponyStack numberStack;
InitStack(&numberStack, 4);
while (decimalNumber)
{
nTemp = (int)decimalNumber%8;
Push(&numberStack, &nTemp);
decimalNumber = decimalNumber/8;
}
nCount = CountOfStack(numberStack);//元素个数也就是位数
while(!IsEmptyStack(numberStack))
{
Pop(&numberStack, &nTemp);
octNumber += (nTemp*pow(10.0, --nCount));
}
DestroyStack(&numberStack);
return (int)octNumber;
}
~~~
再来看一个行编辑程序的示例,用户在终端输入字符,完成后保存用户的数据区, 因为在输入的过程中可能出错,需要修改,所以不可能每输入一个字符就存入数据区。比较好的做法是先在内存里开一个输入的缓冲区,当用户输入完成一行后,再存入数据区。在行内可以修改。例如,当用户发现刚输入的一个字符是错的之后,可以再输入一个'#',表示前一个字符是错的,如果发现当前行输入的错误太多,可以输入一个退行符'@',表示当前行都无效,举个例子:
whli#ilr#e(s#*s)
outcha@putchar(*s=#++)
实际有效的字符是这样的:
while(*s)
putchar(*s++)
可以把内存里这个输入缓冲区定为栈,正常情况下每输入一个字符直接入栈,如果发现字符是'#',就栈顶pop一次,如果是'@'就清空栈.代码实现如下:
~~~
void lineEdit()
{
char ch = 0;
char chTemp = 0;
ponyStack lineStack;
InitStack(&lineStack, 1);
ch = getchar();
while (ch != EOF)
{
while (ch != EOF && ch != '\n')
{
switch (ch)
{
case '#':
Pop(&lineStack, &chTemp);
break;
case '@':
ClearStack(&lineStack);
break;
default:
Push(&lineStack, &ch);
break;
}
ch = getchar();
}
writeToFile(lineStack);//存数据
ClearStack(&lineStack);//准备接收下一行
if (ch != EOF)
{
ch = getchar();
}
}
DestroyStack(&lineStack);
}
~~~
最后一个例子是表达式求值的算法,这个在计算器应用中比较多用到。比如,
求+4*9-16/4
建两个栈,一个存操作数,一个存运算符.为简单起,在运算符栈会预先存一个'#',表示表达式开始,然后以'#'结束。运算规则是这样的:
输入字符,如果是'#',则结束,如果是操作数,直接进操作数栈。如果是运算符,则跟栈顶的运算符比较,如果栈顶的优先级低,直接进栈,接收下一字符,如果相等,脱括号,接收下一个字符,如果栈顶的优先级高,pop两个操作数,pop栈内操作符,运算,然后运算的结果进操作数栈。当前运算符继续跟栈顶比较。
要实现这个代码,首先要有一个表格,存储我们操作符之间的优先级关系,如下所示:
~~~
static char priority[7][7] = {
'>','>','<','<','<','>','>', // +
'>','>','<','<','<','>','>', // -
'>','>','>','>','<','>','>', // *
'>','>','>','>','<','>','>', // /
'<','<','<','<','<','=',' ', // (
'>','>','>','>',' ','>','>', // )
'<','<','<','<','<',' ','=', // #
};// + - * / ( ) #
~~~
然后实现根据上面的思路,实现起来就比较容易了:
~~~
int evaluateExpression()
{
char chCurrent = 0;
char chOnTop = 0;
char chTemp = 0;
int nResult = 0;
int nTemp = 0;
int a,b;
int nOperandFlag = 0;
ponyStack operatorStack;//运算符栈
ponyStack operandStack; //操作数栈
InitStack(&operatorStack, 1);
chTemp = '#';
Push(&operatorStack, &chTemp);
InitStack(&operandStack, 4);
chCurrent = getchar();
GetTop(operatorStack, &chOnTop);
while ((chCurrent != '#')||(chOnTop != '#'))
{
if (!isOperator(chCurrent))//是操作数,要考虑多位整型数的情况
{
nTemp = nTemp * (int)pow(10.0, nOperandFlag);
nTemp += (int)(chCurrent - '0');
chCurrent = getchar();
nOperandFlag = 1;
}
else
{
if (nOperandFlag == 1)
{
Push(&operandStack, &nTemp);//操作数输入结束,入栈
nOperandFlag = 0;
nTemp = 0;
}
GetTop(operatorStack, &chOnTop);
switch (precede(chOnTop, chCurrent))//比较优先级
{
case '<': //栈顶的优先级小
Push(&operatorStack, &chCurrent);
chCurrent = getchar();
GetTop(operatorStack, &chOnTop);
break;
case '=': //脱括号,接收下个字符
Pop(&operatorStack, &chTemp);
chCurrent = getchar();
GetTop(operatorStack, &chOnTop);
break;
case '>': //栈顶的优先级大,出栈运算,结果入栈
{
Pop(&operandStack, &a);
Pop(&operandStack, &b);
Pop(&operatorStack, &chTemp);
nTemp = operate(a, chTemp, b);
Push(&operandStack, &nTemp);
nTemp = 0;//重置
GetTop(operatorStack, &chOnTop);
}
break;
default:
break;
}
}
}
GetTop(operandStack, &nResult);
DestroyStack(&operatorStack);
DestroyStack(&operandStack);
return nResult;
}
~~~
代码下载地址:
https://github.com/pony-maggie/StackDemo
或
http://download.csdn.net/detail/pony_maggie/7499167
前言
最后更新于:2022-04-01 15:58:05
> 原文出处:[那些年,一起追过的算法](http://blog.csdn.net/column/details/chasing-algorithm.html)
作者:[pony_maggie](http://blog.csdn.net/pony_maggie)
**本系列文章经作者授权在看云整理发布,未经作者允许,请勿转载!**
# 那些年,一起追过的算法
> 希望借助这个专栏,能集合各种常用,经典的算法,为自己学习,也为分享给在校大学生或者已经工作的IT从业者。可以结合以下书籍阅读本专栏:
<<数据结构>>(严蔚敏版),<<算法设计与分析>>(郑宗汉版)