对于你比较熟悉的两个问题,描述不同的求解算法,对不同算法的效率进行理论分析,同时对算法进行仿真或编程实现,显示仿真结果或程序运行结果,最后对算法效率的理论分析结果和实验结果(进行对比、分析)。

题目
对于你比较熟悉的两个问题,描述不同的求解算法,对不同算法的效率进行理论分析,同时对算法进行仿真或编程实现,显示仿真结果或程序运行结果,最后对算法效率的理论分析结果和实验结果(进行对比、分析)。
问题一:排序问题
问题描述
输入一个整数的序列,给其排按照从小到大的顺序排序,并输出。
解决方案
方法一:冒泡排序
(1)算法思路:
冒泡排序,它重复地走访过要排序的元素列,依次比较两个相邻的元素,如果顺序(如从大到小、首字母从从Z到A)错误就把他们交换过来。走访元素的工作是重复地进行直到没有相邻元素需要交换,也就是说该元素列已经排序完成。具体的步骤如下:
(1)比较相邻的元素。如果第一个比第二个大,就交换他们两个。
(2)对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。在这一点,最后的元素应该会是最大的数。
(3) 针对所有的元素重复以上的步骤,除了最后一个。
(4)持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
(2)代码实现:

#include <stdio.h>
#define ARR_LEN 255 /*数组长度上限*/
#define elemType int /*元素类型*/
void bubbleSort (elemType arr[], int len) {
    elemType temp;
    int i, j;
    for (i=0; i<len-1; i++) /* 外循环为排序趟数,len个数进行len-1趟 */
        for (j=0; j<len-1-i; j++) { /* 内循环为每趟比较的次数,第i趟比较len-i次 */
            if (arr[j] > arr[j+1]) { /* 相邻元素比较,若逆序则交换(升序为左大于右,降序反之) */
                temp = arr[j];
                arr[j] = arr[j+1];
                arr[j+1] = temp;
            }
        }
}
 
int main (void) {
    elemType arr[ARR_LEN] = {3,5,1,-7,4,9,-6,8,10,4};
    int len = 10;
    int i;
     
    bubbleSort (arr, len);
    for (i=0; i<len; i++)
        printf ("%d ", arr[i]);
    putchar ('\n');
     
    return 0;
}

运行结果:
在这里插入图片描述
(3)算法分析:
时间复杂度:
若文件的初始状态是正序的,一趟扫描即可完成排序。所需的关键字比较次数 和记录移动次数 均达到最小值: Cmin=n-1,Mmin=0 。
所以,冒泡排序最好的时间复杂度为O(n) 。
若初始文件是反序的,需要进行 趟排序。每趟排序要进行 次关键字的比较(1≤i≤n-1),且每次比较都必须移动记录三次来达到交换记录位置。在这种情况下,比较和移动次数均达到最大值:
在这里插入图片描述

冒泡排序的最坏时间复杂度为O(n^2) 。
综上,因此冒泡排序总的平均时间复杂度为 O(n^2).
算法稳定性:
冒泡排序就是把小的元素往前调或者把大的元素往后调。比较是相邻的两个元素比较,交换也发生在这两个元素之间。所以,如果两个元素相等,是不会再交换的;如果两个相等的元素没有相邻,那么即使通过前面的两两交换把两个相邻起来,这时候也不会交换,所以相同元素的前后顺序并没有改变,所以冒泡排序是一种稳定排序算法。
方法二:快速排序
(1)算法思路:
随机找出一个数,可以随机取,也可以取固定位置,一般是取第一个或最后一个称为基准,然后就是比基准小的在左边,比基准大的放到右边,如何放做,就是和基准进行交换,这样交换完左边都是比基准小的,右边都是比较基准大的,这样就将一个数组分成了两个子数组,然后再按照同样的方法把子数组再分成更小的子数组,直到不能分解为止。
(2)代码实现:

void sort(int *a, int left, int right)
{
    if(left >= right)/*如果左边索引大于或者等于右边的索引就代表已经整理完成一个组了*/
    {
        return ;
    }
    int i = left;
    int j = right;
    int key = a[left];
     
    while(i < j)                               /*控制在当组内寻找一遍*/
    {
        while(i < j && key <= a[j])
        /*而寻找结束的条件就是,1,找到一个小于或者大于key的数(大于或小于取决于你想升
        序还是降序)2,没有符合条件1的,并且i与j的大小没有反转*/ 
        {
            j--;/*向前寻找*/
        }
         
        a[i] = a[j];
        /*找到一个这样的数后就把它赋给前面的被拿走的i的值(如果第一次循环且key是
        a[left],那么就是给key)*/
         
        while(i < j && key >= a[i])
        /*这是i在当组内向前寻找,同上,不过注意与key的大小关系停止循环和上面相反,
        因为排序思想是把数往两边扔,所以左右两边的数大小与key的关系相反*/
        {
            i++;
        }
         
        a[j] = a[i];
    }
     
    a[i] = key;/*当在当组内找完一遍以后就把中间数key回归*/
    sort(a, left, i - 1);/*最后用同样的方式对分出来的左边的小组进行同上的做法*/
    sort(a, i + 1, right);/*用同样的方式对分出来的右边的小组进行同上的做法*/
                       /*当然最后可能会出现很多分左右,直到每一组的i = j 为止*/
}

运行结果:
在这里插入图片描述
(3)算法分析:
快速排序的一次划分算法从两头交替搜索,直到low和high重合,因此其时间复杂度是O(n);而整个快速排序算法的时间复杂度与划分的趟数有关。
理想的情况是,每次划分所选择的中间数恰好将当前序列几乎等分,经过log2n趟划分,便可得到长度为1的子表。这样,整个算法的时间复杂度为O(nlog2n)。
最坏的情况是,每次所选的中间数是当前序列中的最大或最小元素,这使得每次划分所得的子表中一个为空表,另一子表的长度为原表的长度-1。这样,长度为n的数据表的快速排序需要经过n趟划分,使得整个排序算法的时间复杂度为O(n2)。
为改善最坏情况下的时间性能,可采用其他方法选取中间数。通常采用“三者值取中”方法,即比较H->r[low].key、H->r[high].key与H->r[(10w+high)/2].key,取三者中关键字为中值的元素为中间数。
最好的情况下,即快速排序的每一趟排序都将元素序列均匀地分割成长度相近的两个子表,所需栈的最大深度为log2(n+1);但最坏的情况下,栈的最大深度为n。这样,快速排序的空间复杂度为O(log2n))。
方法三:选择排序
(1)算法思路:
对比数组中前一个元素跟后一个元素的大小,如果后面的元素比前面的元素小则用一个变量k来记住他的位置,接着第二次比较,前面“后一个元素”现变成了“前一个元素”,继续跟他的“后一个元素”进行比较如果后面的元素比他要小则用变量k记住它在数组中的位置(下标),等到循环结束的时候,我们应该找到了最小的那个数的下标了,然后进行判断,如果这个元素的下标不是第一个元素的下标,就让第一个元素跟他交换一下值,这样就找到整个数组中最小的数了。然后找到数组中第二小的数,让他跟数组中第二个元素交换一下值,以此类推。
(2)代码实现:

void select_sort(int*a,int n)
{
    register int i,j,min,t;
    for(i=0;i<n-1;i++)
    {
        min=i;//查找最小值
        for(j=i+1;j<n;j++)
            if(a[min]>a[j])
                min=j;//交换
        if(min!=i)
        {
            t=a[min];
            a[min]=a[i];
            a[i]=t;
        }
    }
}

运行结果:
在这里插入图片描述
(3)算法分析:
时间复杂度:
选择排序的交换操作介于 0 和 (n - 1) 次之间。选择排序的比较操作为 n (n - 1) / 2 次之间。选择排序的赋值操作介于 0 和 3 (n - 1) 次之间。比较次数O(n^2),比较次数与关键字的初始状态无关,总的比较次数N=(n-1)+(n-2)+…+1=n*(n-1)/2。交换次数O(n),最好情况是,已经有序,交换0次;最坏情况交换n-1次,逆序交换n/2次。交换次数比冒泡排序少多了,由于交换所需CPU时间比比较所需的CPU时间多,n值较小时,选择排序比冒泡排序快。
稳定性:
选择排序是给每个位置选择当前元素最小的,比如给第一个位置选择最小的,在剩余元素里面给第二个元素选择第二小的,依次类推,直到第n-1个元素,第n个元素不用选择了,因为只剩下它一个最大的元素了。那么,在一趟选择,如果一个元素比当前元素小,而该小的元素又出现在一个和当前元素相等的元素后面,那么交换后稳定性就被破坏了。比较拗口,举个例子,序列5 8 5 2 9,我们知道第一遍选择第1个元素5会和2交换,那么原序列中两个5的相对前后顺序就被破坏了,所以选择排序是一个不稳定的排序算法。
方法四:插入排序
(1)算法思路:
假定这个数组的序是排好的,然后从头往后,如果有数比当前外层元素的值大,则将这个数的位置往后挪,直到当前外层元素的值大于或等于它前面的位置为止.这具算法在排完前k个数之后,可以保证a[1…k]是局部有序的,保证了插入过程的正确性.
(2)代码实现:

void insertion_sort(int array[],int first,int last)
{
    int i,j;
    int temp;
    for(i=first+1;i<last;i++)
    {
        temp=array[i];
        j=i-1;
        //与已排序的数逐一比较,大于temp时,该数移后
        while((j>=0)&&(array[j]>temp))
        {
            array[j+1]=array[j];
            j--;
        }
        //存在大于temp的数
        if(j!=i-1)
            array[j+1]=temp;
    }
}

运行结果:
在这里插入图片描述
(3)算法分析:
如果目标是把n个元素的序列升序排列,那么采用插入排序存在最好情况和最坏情况。最好情况就是,序列已经是升序排列了,在这种情况下,需要进行的比较操作需(n-1)次即可。最坏情况就是,序列是降序排列,那么此时需要进行的比较共有n(n-1)/2次。插入排序的赋值操作是比较操作的次数加上 (n-1)次。平均来说插入排序算法的时间复杂度为O(n^2)。
插入排序是在一个已经有序的小序列的基础上,一次插入一个元素。当然,刚开始这个有序的小序列只有1个元素,就是第一个元素。比较是从有序序列的末尾开始,也就是想要插入的元素和已经有序的最大者开始比起,如果比它大则直接插入在其后面,否则一直往前找直到找到它该插入的位置。如果碰见一个和插入元素相等的,那么插入元素把想插入的元素放在相等元素的后面。所以,相等元素的前后顺序没有改变,从原无序序列出去的顺序就是排好序后的顺序,所以插入排序是稳定的。
4个排序算法对比:
在前面的介绍和分析中我们提到了冒泡排序、选择排序、插入排序,快速排序四种简单的排序。我们可以知道排序算法要么简单有效,要么是利用简单排序的特点加以改进,要么是以空间换取时间在特定情况下的高效排序。但是这些排序方法都不是固定不变的,需要结合具体的需求和场景来选择甚至组合使用。才能达到高效稳定的目的。没有最好的排序,只有最适合的排序。
冒泡排序就是把小的元素往前调或者把大的元素往后调。比较是相邻的两个元素比较,交换也发生在这两个元素之间。所以,如果两个元素相等,是不会再交换的;如果两个相等的元素没有相邻,那么即使通过前面的两两交换把两个相邻起来,这时候也不会交换,所以相同元素的前后顺序并没有改变,所以冒泡排序是一种稳定排序算法。
从空间性能上看,尽管快速排序只需要一个元素的辅助空间,但快速排序需要一个栈空间来实现递归。可以证明,快速排序的平均时间复杂度也是O(nlog2n)。因此,该排序方法被认为是目前最好的一种内部排序方法。
插入排序不适合对于数据量比较大的排序应用。但是,如果需要排序的数据量很小,例如,量级小于千,那么插入排序还是一个不错的选择。
下表为各算法的各数据对比分析:
算法名	平均时间复杂度	算法用时时间(ms)	最好情况	最坏情况	稳定性冒泡排序	O(n^2)	52.98	O(n)	O(n^2)	稳定快速排序	O(nlog2n)	66.15	O(nlogn)	O(n^2)	不稳定选择排序	O(n^2)	55.32	O(n^2)	O(n^2)	不稳定插入排序	O(n^2)	54.44	O(n)	O(n^2)	稳定

  1. 从平均时间来看,冒泡排序是效率最高的,但快速排序在最坏情况下的时间性能不如其他排序。而后者相比较的结果是,在n较大时归并排序使用时间较少,但使用辅助空间较多。
  2. 上面说的简单排序,其中直接插入排序最简单,但序列基本有序或者n较小时,直接插入排序是好的方法,因此常将它和其他的排序方法,如快速排序、归并排序等结合在一起使用。
  3. 上面的算法实现大多数是使用线性存储结构,像插入排序这种算法用链表实现更好,省去了移动元素的时间。具体的存储结构在具体的实现版本中也是不同的。

问题二:字符串匹配
首先是一系列概念定义:
文本Text: 是一个长度为n的数组T[1…n] (这里第一位置索引是数字1)
模式Pattern: 是一个长度为m的数组P[1…m], 并且m<=n.
T和P的元素都属于有限的字母表Σ 表
概念:有效位移Valid Shift(用字母s代表)。即P在T中出现,并且位置移动s次。如果0<= s <= n-m ,并且T[s+1…s+m] = P[1…m],则s是有效位移。

在这里插入图片描述
上图的有效位移是3。
问题描述
给定一个目标串str和模式串ptr,要求寻找ptr第一次在str出现的位置,并返回其下标,匹配不到则返回-1。
解决方案
方法一:暴力检索法(BF算法)
(1)算法思路:
其算法思想很简单,从主串S的第pos个字符开始,和模式串T的第一个字符进行比较,若相等,则主串和模式串都后移一个字符继续比较;若不相同,则回溯到主串S的第pos+1个字符重新开始比较。
依次类推,直到模式串T中每个字符依次和主串S中的一个连续字符串全部相等时,则匹配成功,返回模式串T的第一个字符在主串S中的位置;若主串遍历完也没有成功,则匹配失败。
(2)代码实现:

#include <stdio.h>
#include <string.h>
void BF(const char* T,const char* P)
{
    int n = strlen(T);
    int m = strlen(P);
    for (int s = 0; s < n - m+1; ++s)
    {
        int i = s, j = 0;
        for (;j < m-1;)
        {
            if (T[i] == P[j]) ++i, ++j;
            else break;
        }
        if (T[i] == P[j])
            printf("%d\n",s);       
    }
}
int main()
{
	char str[]="abcababcababcababcabaabcabac",ptr[]="abaa";
	printf("目标串为:%s\n",str);
	printf("模式串为:%s\n",ptr);
	BF(str,ptr);
    return 0;
}

运行结果:
在这里插入图片描述
(3)算法分析:
空间复杂度:O(1);
时间复杂度:O(N*M),N是目标串的长度,M是模式串的长度;
最坏情况下,对每一个s都需要做m次(模式P的长度为m)的比较。则算法的上届是O((n-m+1)*m)。到后面我们会看到朴素算法之所以慢,是因为它只是关心有效的位移,而忽略其它无效的位移。当一次位移s被验证是无效的之后,它只是向右位移1位,然后从头开始继续下一次的比较。这样做完全没有利用到之前已经匹配的信息,而这些信息有时候会很有用。
方法二:KMP算法
(1)算法思路:
KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。具体实现就是通过一个next()函数实现,函数本身包含了模式串的局部匹配信息。KMP算法的时间复杂度O(m+n)。
(2)代码实现:

void Get_next() 
{                       
        int len = sizeof(ptr);
        next[0] = -1;
        int j = -1;     
 
        for(int i = 1; i < len; ++i)
        {               
                while(j > -1 && ptr[i] != ptr[j+1]) //不等next等j
                        j = next[j];
 
                if( ptr[i] == ptr[j+1] ) // 等子串j加加
                        j++;
                next[i] = j;
        }
 
//        for(int i = 0; i < len; ++i)
//                cout << next[i] << ' ';
//                cout << endl;
}
 
int KMP( )
{
        Get_next();
        int s_len = strlen(str);
        int p_len = strlen(ptr);
        int j  = -1;
        for(int i = 0; i < s_len; ++i)
        {
                while(j > -1 && ptr[j+1] != str[i])
                        j = next[j];
        
                if(ptr[j+1] == str[i])
                        j++;
        
                if(j == p_len-1) 
                        return i-p_len+1;
        }
        return -1;
} 

运行结果:
在这里插入图片描述
(3)算法分析:
KMP算法的时间复杂度是线性的。首先是kmp函数。显然决定kmp函数时间复杂度的变量仅仅有两个,i和j,当中i仅仅添加了len次,是O(len)的,以下讨论j,由于由next数组的定义我们知道next[j]<j,所以在回溯的时候j至少减去了1,而且j保证是个非负数。另外。由代码可知j最多添加了len次,且每次仅仅添加了1。简单来说。j每次添加仅仅能添加1,每次减小至少减去1,而且保证j是个非负数。那么可知j减小的次数一定不能超过添加的次数。所以。回溯的次数不会超过len。
综上所述。kmp函数的时间复杂度为O(len)。同理。对于计算next数组相同用相似的方法证明它的时间复杂度为O(len),这里不再赘述。对于长度为n的原始串S。和长度为m的模式串T,KMP算法的时间复杂度为O(n+m)。
方法三:BM算法
坏字符规则:当文本串中的某个字符跟模式串的某个字符不匹配时,我们称文本串中的这个失配字符为坏字符,此时模式串需要向右移动,移动的位数 = 坏字符在模式串中的位置 - 坏字符在模式串中最右出现的位置。此外,如果”坏字符”不包含在模式串之中,则最右出现位置为-1。
好后缀规则:当字符失配时,后移位数 = 好后缀在模式串中的位置 - 好后缀在模式串上一次出现的位置,且如果好后缀在模式串中没有再次出现,则为-1。
(1)算法思路:
Boyer-Moore充分使用预处理P的信息来尽可能跳过更多的字符。通常,我们比较一个字符串都是从首字母开始,逐个比较下去。一旦发现有不同的字符,就需要从头开始进行下一次比较。这样,就需要将字串中的所有字符一一比较。Boyer-Moore算法的关键在于,当P的最后一个字符被比较完成后,我们可以决定跳过一个或更多个字符。如果最后一个字符不匹配,那么就没必要继续比较前一个字符。如果最后一个字符未在P中出现,那么我们可以直接跳过T的n个字符,比较接下来的n个字符,n为P的长度(见定义)。如果最后一个字符出现在P中,那么跳过的字符数需要进行计算(也就是将P整体往后移),然后继续前面的步骤来比较。通过这种字符的移动方式来代替逐个比较是这个算法如此高效的关键所在。
(2)代码实现:

void BM(char *x, int m, char *y, int n) {
   int i, j, bmGs[XSIZE], bmBc[ASIZE];
 
   /* Preprocessing */
   preBmGs(x, m, bmGs);
   preBmBc(x, m, bmBc);
 
   /* Searching */
   j = 0;
   while (j <= n - m) {
      for (i = m - 1; i >= 0 && x[i] == y[i + j]; --i);
      if (i < 0) {
         OUTPUT(j);
         j += bmGs[0];
      }
      else
         j += MAX(bmGs[i], bmBc[y[i + j]] - m + 1 + i);
   }
} 

运行结果:
![在这里插入图片描述](https://img-blog.csdnimg.cn/20200107165337686.png在这里插入图片描述
(3)算法分析:
它在最坏情况下找到模式所有出现的时间复杂度为O(mn),在最好情况下执行匹配找到模式所有出现的时间复杂度为O(n)。
3个算法对比:
下表这三个字符串匹配算法的各数据对比分析:
算法名	平均时间复杂度	算法用时时间(ms)	最好情况	最坏情况BF算法	O(n*m)	52.78	O(n)	O(n*m)KMP算法	O(n+m)	80.27	O(n)	O(n+m)BM算法	O(n)	84.03	O(n)	O(n*m)
暴力检索法是最好想到的算法,也最好实现,在情况简单的情况下可以直接使用。
KMP算法是对暴力检索法的改进,KMP 算法预先计算出了一个哈希表,用来指导在匹配过程中匹配失败后尝试下次匹配的起始位置,以此避免重复的读入和匹配过程。但是,它并不是效率最高的算法,实际采用并不多。各种记事本的“查找”功能(CTRL + F)一般都是采用的此算法。
BM算法的思想在字符串匹配中很重要,其中好后缀与KMP算法的思想不谋而合,而坏字符跳跃,跟Sunday算法很相似。总的来说,BM是这三个算法中最高效的匹配算法。BM算法比kmp算法更高效,BM算法的执行效率要比KMP算法快3-5倍左右。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值