数据结构与算法(二) 排序

本文深入浅出地介绍了希尔排序、快速排序等排序算法的原理与代码实现,并详细解析了KMP算法的工作机制及应用,帮助读者掌握高效算法的设计思想。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

希尔排序-重点

原理——分组

希尔排序在数组中采用跳跃式分组的策略,通过某个增量将数组元素划分为若干组,然后分组进行插入排序,随后逐步缩小增量,继续按组进行插入排序操作,直至增量为1。
希尔排序的基本步骤,在此我们选择增量gap=length/2,缩小增量继续以gap = gap/2的方式,这种增量选择我们可以用一个序列来表示,{n/2,(n/2)/2…1},称为增量序列。
一个希尔排序的示例

代码实现

/*
功能:实现希尔排序
@param: data:待排序的数据的地址
		length:数据的长度
*/
int shell_sort(int *data, int length) {

    int gap = 0; //分组的跨度
    int i = 0, j = 0;

    for (gap = length / 2; gap >= 1;gap /= 2) { // 分组的次数

        for(i = gap; i < length; i ++) { // 每组遍历 

            int temp = data[i];
            for (j = i - gap; j >= 0 && temp < data[j];j = j - gap) { //组内排序

                data[j+gap] = data[j];

            }

            data[j+gap] = temp;
        }

    }
    return 0;
}

参考_希尔排序

快速排序-重点

原理——大数在基准数右边,小数在基准数左边

任意选取一个数据(通常选用数组的第一个数)作为关键数据,然后将所有比它小的数都放到它左边,所有比它大的数都放到它右边(最终结果),这个过程称为一趟快速排序。(设两个指示标志: i 指向起始位置,j 指向末尾,来帮助寻找大数和小数)
一句话描述算法:
选择最左端的值为哨兵key,i,j分别指向最左边,最右边。j从右向左找比key小的值,找到了,j位置的值覆盖到i位置上;i从左往右找比key大的值,找到了,i位置的值覆盖到j位置上;这样一直循环,当i,j相遇时,就是key的位置,用key将其值覆盖;然后以key的位置为界,左,右分别递归快速排序。

P1
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

代码实现

/*
功能:实现快速排序
参数: data:待排序数据的地址
	  left:左标志,通常也设为基准值,找比基准值大的数
	  right:右标志, 找比基准值小的数
*/
int sort(int *data, int left, int right) { //每一次递归, 每调用一次, 确定一个值得正确位置

    if (left >= right) return 0;

    int i = left;
    int j = right;
    int key = data[left];

    while (i < j) { //

        while (i < j && key < data[j]) { // 从右往左找比key哨兵值要小的位置
            j --;
        }
        data[i] = data[j]; //将找到位置的值将i位置的值覆盖掉

        while (i < j && key >= data[i]) { //从左往右找比Key哨兵值要大的位置
            i ++;
        }
        data[j] = data[i]; //找到位置的值将j位置的值覆盖掉
    }
    // i == j
    data[i] = key; //i与j相遇时,就是key哨兵的位置

    //然后继续以新位置为界,左边,右边分快递归速排序
    sort(data, left, i-1);
    sort(data, i+1, right);

}

冒泡排序:
原理:最左边的元素和右边元素比较,若更大,则交换;此时第二个与右边元素比较,若大就交换,依次类推,一直检测到最后一个元素;然后开始新一轮比较;

KMP算法

出现字符匹配的问题,优先想到kmp;

解决的问题:在一个字符串(n)中寻找一个子串(m)第一个位置。

特点

原始的查找字符串是匹配过程失败后,单纯的把模式串后移一位,没有利用到已有匹配信息;KMP算法是通过匹配失败后的信息,检测当前字符的前一位的字符所在的字符串信息,来决定右移的步数;

原理

背景:
子串中每个字符都有个信息:
到此字符的为止,来统计前后缀的公共字符串的长度,就是下次匹配时需移动的步长;
我们将此信息存储在next数组中记录下来,当匹配时发现某处的字符不相同,只需取出该处前一位的next数值,进行移动,就可以再次匹配了,省时省力。

其关键是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的明。
只是在每次匹配过程中发生某次失配时,不再单纯地把模式后移一位,而是根据当前字符的特征数来决定模式右移的位数。
模式串回退多少,可以由之前的对比得到
主串:abcabcabcabcabcd
模式串:abcabcd
前缀:不包含最后一个字符,前面的所有集合
后缀:不包含第一个字符的,所有字符集合
在这里插入图片描述
前缀、后缀的公共元素,这个元素的长度,就是模式串基于上一次向右边回溯的个数来进行下一次的匹配(移动的步长)。

难点:如何求取next数组值?next数组是部分匹配表,也就是字符串头部和尾部有多少重复的字符串个数。
其实就是来比较前后缀是否相同,记录相同元素的长度。

// abcabc
// k = 0 指向前缀
// q = 1 指向后缀
void make_next(const char *pattern, int *next) { //此函数时想要找到前后缀公共元素的长度,也就是移动步数

    int q, k;
    int m = strlen(pattern);

    next[0] = 0; // 默认值为0,此数组代表
    for (q = 1,k = 0;q < m; q ++) { 
        while (k > 0 && pattern[q] != pattern[k]) { 
            k = next[k-1]; //当开始不匹配时,利用已有的next开始回退已匹配的字符串,直到遇见相同的字符或者退回到k=0的位置
        }

        if (pattern[q] == pattern[k]) { // 如果前缀与后缀有相同的字符
            k ++; //代表k可移动的步数
        }

        next[q] = k;
        
    }
}
int kmp(const char *text, const char *pattern, int *next) {

    int n = strlen(text);
    int m = strlen(pattern);

    make_next(pattern, next);

    int i, q;
    for (i = 0, q = 0;i < n;i ++) { //i --> text, q --> pattern i遍历主串,q遍历字串
#if 1
// 之所以是next[j-1],是因为 next[j-1]的最长公共子串,
        // 同样也是pattern[j]字符串的公共子串,只是不是最长的而已。(根据对称性很容易得到
        while (q > 0 && pattern[q] != text[i]) { //当 进行回溯,回到之前的位置
            q = next[q-1];
        }
#endif 
        if (pattern[q] == text[i]) { //先从这里看,
            q ++;
        }
        // q == m --->  如果q加的过程,长度与pattern相同时,就匹配完了
        if (q == m) {
            return i-q+1;
        }
    }
    return -1;
}

以下仅供参考加以理解,以上为准。
参考:
作者:海纳
链接:https://www.zhihu.com/question/21923021/answer/281346746
来源:知乎
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

有些算法,适合从它产生的动机,如何设计与解决问题这样正向地去介绍。但KMP算法真的不适合这样去学。最好的办法是先搞清楚它所用的数据结构是什么,再搞清楚怎么用,最后为什么的问题就会有恍然大悟的感觉。我试着从这个思路再介绍一下。大家只需要记住一点,PMT是什么东西。然后自己临时推这个算法也是能推出来的,完全不需要死记硬背。KMP算法的核心,是一个被称为部分匹配表(Partial Match Table)的数组。我觉得理解KMP的最大障碍就是很多人在看了很多关于KMP的文章之后,仍然搞不懂PMT中的值代表了什么意思。这里我们抛开所有的枝枝蔓蔓,先来解释一下这个数据到底是什么。对于字符串“abababca”,它的PMT如下表所示:就像例子中所示的,如果待匹配的模式字符串有8个字符,那么PMT就会有8个值。我先解释一下字符串的前缀和后缀。如果字符串A和B,存在A=BS,其中S是任意的非空字符串,那就称B为A的前缀。例如,”Harry”的前缀包括{”H”, ”Ha”, ”Har”, ”Harr”},我们把所有前缀组成的集合,称为字符串的前缀集合。同样可以定义后缀A=SB, 其中S是任意的非空字符串,那就称B为A的后缀,例如,”Potter”的后缀包括{”otter”, ”tter”, ”ter”, ”er”, ”r”},然后把所有后缀组成的集合,称为字符串的后缀集合。要注意的是,字符串本身并不是自己的后缀。有了这个定义,就可以说明PMT中的值的意义了。PMT中的值是字符串的前缀集合与后缀集合的交集中最长元素的长度。例如,对于”aba”,它的前缀集合为{”a”, ”ab”},后缀 集合为{”ba”, ”a”}。两个集合的交集为{”a”},那么长度最长的元素就是字符串”a”了,长 度为1,所以对于”aba”而言,它在PMT表中对应的值就是1。再比如,对于字符串”ababa”,它的前缀集合为{”a”, ”ab”, ”aba”, ”abab”},它的后缀集合为{”baba”, ”aba”, ”ba”, ”a”}, 两个集合的交集为{”a”, ”aba”},其中最长的元素为”aba”,长度为3。 好了,解释清楚这个表是什么之后,我们再来看如何使用这个表来加速字符串的查找,以及这样用的道理是什么。如图 1.12 所示,要在主字符串"ababababca"中查找模式字符串"abababca"。如果在 j 处字符不匹配,那么由于前边所说的模式字符串 PMT 的性质,主字符串中 i 指针之前的 PMT[j −1] 位就一定与模式字符串的第 0 位至第 PMT[j−1] 位是相同的。这是因为主字符串在 i 位失配,也就意味着主字符串从 i−j 到 i 这一段是与模式字符串的 0 到 j 这一段是完全相同的。而我们上面也解释了,模式字符串从 0 到 j−1 ,在这个例子中就是”ababab”,其前缀集合与后缀集合的交集的最长元素为”abab”, 长度为4。所以就可以断言,主字符串中i指针之前的 4 位一定与模式字符串的第0位至第 4 位是相同的,即长度为 4 的后缀与前缀相同。这样一来,我们就可以将这些字符段的比较省略掉。具体的做法是,保持i指针不动,然后将j指针指向模式字符串的PMT[j −1]位即可。简言之,以图中的例子来说,在 i 处失配,那么主字符串和模式字符串的前边6位就是相同的。又因为模式字符串的前6位,它的前4位前缀和后4位后缀是相同的,所以我们推知主字符串i之前的4位和模式字符串开头的4位是相同的。就是图中的灰色部分。那这部分就不用再比较了。有了上面的思路,我们就可以使用PMT加速字符串的查找了。我们看到如果是在 j 位 失配,那么影响 j 指针回溯的位置的其实是第 j −1 位的 PMT 值,所以为了编程的方便, 我们不直接使用PMT数组,而是将PMT数组向后偏移一位。我们把新得到的这个数组称为next数组。下面给出根据next数组进行字符串匹配加速的字符串匹配程序。其中要注意的一个技巧是,在把PMT进行向右偏移时,第0位的值,我们将其设成了-1,这只是为了编程的方便,并没有其他的意义。在本节的例子中,next数组如下表所示。具体的程序如下所示:int KMP(char * t, char * p)
{
int i = 0;
int j = 0;

while (i < (int)strlen(t) && j < (int)strlen(p))
{
	if (j == -1 || t[i] == p[j]) 
	{
		i++;
       		j++;
	}
 	else 
       		j = next[j];
	}

if (j == strlen(p))
   return i - j;
else 
   return -1;

}
好了,讲到这里,其实KMP算法的主体就已经讲解完了。你会发现,其实KMP算法的动机是很简单的,解决的方案也很简单。远没有很多教材和算法书里所讲的那么乱七八糟,只要搞明白了PMT的意义,其实整个算法都迎刃而解。现在,我们再看一下如何编程快速求得next数组。其实,求next数组的过程完全可以看成字符串匹配的过程,即以模式字符串为主字符串,以模式字符串的前缀为目标字符串,一旦字符串匹配成功,那么当前的next值就是匹配成功的字符串的长度。具体来说,就是从模式字符串的第一位(注意,不包括第0位)开始对自身进行匹配运算。 在任一位置,能匹配的最长长度就是当前位置的next值。如下图所示。求next数组值的程序如下所示:void getNext(char * p, int * next)
{
next[0] = -1;
int i = 0, j = -1;

while (i < (int)strlen(p))
{
	if (j == -1 || p[i] == p[j])
	{
		++i;
		++j;
		next[i] = j;
	}	
	else
		j = next[j];
}

}
至此,KMP算法就全部介绍完了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值