回文串分割问题-回溯和C语言多级指针应用

回文串分割-回溯和C语言多级指针应用

  1. 前言

本文将探讨回文串分割问题所用到的回溯和C语言多级指针技巧,秉承聚焦学习的理念,本文暂不讨论回文串分割子过程所涉及到的动态规划问题。回溯的最直观的理解是在深度遍历二叉树或多叉树的过程中,在递归函数前对某一变量进行判断,保存在适当的变量中(临时数组或线性表或栈当中),在递归函数退出时,回溯到递归之前的状态。

  1. 问题描述

本问题来源Leetcode习题,给定一个回文串,要求对此回文串进行分割,输出所有的分割方式,并以字符串形式表示。在解答此题之前,首先要了解回文串的概念,回文串是一组字符或数字,其前后字符或数字对称,一般有两种形式:

a) 当字符或数字总数为奇数时,回文串的普遍形式为:
{ a 1 . . . . a i . . . a n } , 那么就有 a n = a 1 , a ( n − 1 ) = a 2 , . . . . a ( n / 2 + 1 ) = a ( n / 2 + 1 ) \{a_1....a_i...a_n\}, 那么就有a_n=a_1, a(n-1)=a_2,....a(n/2+1)=a(n/2+1) {a1....ai...an},那么就有an=a1,a(n1)=a2,....a(n/2+1)=a(n/2+1)
中间的数字将作为镜像点,其两边的间隔相同的元素值相同。

b) 当字符或数字总数为偶数时,回文串的普遍形式:

{a(1),a(2)…a(i),a(i+1)…a(n-1),a(n)}, 那么就有a(n)=a(1),a(n-1)=a(2),a(i+1)=a(i),此时没有严格的中间镜像点,也可以理解为中间镜像点为∅。

以示例对问题进行说明,

输入s=“aab”

输出{“a”,“a”,“b”}, {“aa”,“b”}

值得一提的是,单个字符本身就是回文串,因为其自身和自身永远相等。

  1. 问题解析
  • 回溯算法

把需要分割的字符串分为两部分,已经分割完成的部分和即将分割的部分,假定有字符串s=“a1,a2…ai-1,ai…aj,an-1,an”,其中"a1,a2…ai-1"字符串已经为分割完成的回文子串,接下来需要不断自增判断(ai,aj)区间内的字符串是否为回文串,如果是回文串,那么接着调用(a(j+1),ak)之间的字符串,直至字符串结束,然后需要返回到某个字符串区间,比如(ai,a(j+1))字符串区间。

这是典型的深度优先搜索模式,首先需要对所有最小的字符串遍历至尾部,第一次遍历得到的结构就是字符串中的每个字符都是回文,然后保持起始点不变,结尾的点往前移动,再次进入深度优先模式。如果我们以s="aab"为例,遍历生成的多叉树可以表示为:

在这里插入图片描述

最左边的分支表示第一次遍历,表示一组完整的回文子串;然后进行回退到第二个’a’,此时遍历往后移动一位,需要确认"ab"是否为回文子串,答案很明显,"ab"不是回文子串,那么继续回退至第一个’a’,这时候需要判定第二个分叉’aa’是否为回文串,注意到’aa’回文串,紧接着再采用深度优先遍历,移动到位置’b’,其本身为回文串,再深入下一层,超过字符串的长度,有一次遍历完成,得到第二组回文子串。由于到达尾部结构,继续回退至第一个字符’a’ ,需要比较’aab’是否文回文子串,显而易见’aab’不是回文子串。此时已经深度遍历完成所有的可能,深度遍历至此结束。

值得一提的是,在深度遍历过程中,由于需要保存子回文串的结果,需要引入回溯的概念,在深度遍历的递归函数之前保存遍历的结果至数组/栈/线性表中,递归函数推出后,我们回溯至之前的状态,如果为数组保存,那么数组的下表减1;如果为栈结构,我们就进行出栈操作;如果遇到线性表结构,我们直接调用删除操作,删除线性表的最后一个插入元素。此过程称为回溯,也就是时光倒流的概念,借用一句时髦的歌词,叫做"yesterday once more",这里需要说明的是,递归的步伐是不停息的,回溯的只是对遍历结果的回退。

  • 字符多级指针的使用

在C语言中,如果要表示单个字符,直接利用char类型进行定义即可,举例说明我们要表达’a’字符,那么就可以表示为

char s;
s='a'

接上面继续深入挖掘,那么要表示字符串"aab",需要如何表示呢?

可以有两种表达方式:

char s[4]={'a','a','b','\0'};
----------------------------------
char *s="aab";

第一种方式是采用字符数组,注意事项是,需要把结尾处的’\0’放到字符数组当中去;或者我们采用指针的方式,指针s保存储存"aab"的地址,如果要访问具体的单个字符(右值), 我们可以采用间接运算符(*)对地址进行取值,上面的表达式

char *s="aab";

那么*s+1表示是原地址偏移sizeof(char)的增量,再通过间接运算符(*),可以得知*(s+1)的值为’a’;同理我们如果要更改s+1地址上的值为’b’,就需要用到左值的概念,

*(s+1)=‘b’,此表达式实际上是对s+1地址上的内容进行覆盖操作,可以理解为地址容器,我们替换了地址里面的值。那么,有些深入思考的人就会有疑惑,s+1='b’表达式不是恰好完成了赋值任务吗?s+1代表s指针偏移一个字符大小量,它本质上是地址,而右边’b’本质上是具体的值,两个不同类型是无法完成合理赋值过程的。

继续聊,接着深入挖掘,有好事者就心生疑问,下面表达式如何解读呢?

char **s;

这是准备给s评两星功勋啊在这里插入图片描述

意思是指向字符指针的指针,也可以理解为指向字符串的指针。假定给定这样的形式的数组,

{"abc","def","ghi"};

这就是一个典型的字符串数组,恰好是s指向的对象,赋值操作方法之一为:

#define N 3
char **s;
s=(char **)malloc(sizeof(char*)*N); //allocate 3 char* typte to s
*(s+0)=(char *)malloc(sizeof(char)*(N+1));
*(s+1)=(char *)malloc(sizeof(char)*(N+1));
*(s+2)=(char *)malloc(sizeof(char)*(N+1));
strcpy(*(s+0),"abc");
strcpy(*(s+1),"def");
strcpy(*(s+2),"ghi");

那么加下来我们就要进入激动人心的脑力挑战环节,假定给出如下的定义形式,如何用语言来描述呢?

chra ***s;

好家伙,s君直接晋升到三星上将,距离五星上将还有两级,好好努力,前途一片光明!

这是一个典型的字符串数组的数组,假定有如下数组,s恰好可以指向它,这时候就比较容易理解和进行后续的赋值操作。

{{"ab","cd","ef"},{"gh","ij","kl"}}

这是一个字符串数组的数组,我们上面的三星上将s君就可以指向这个数组的首地址,如果要进行赋值操作,表述形式之一为:

#define N 2

char ***s;
s=(char ***)malloc(sizeof(char **)*N);

*(s+0)=(char **)malloc(sizeof(char*)*(N+1));
*(s+1)=(char **)malloc(sizeof(char*)*(N+1));

*(*(s+0)+0)=(char*)malloc(sizeof(char)*(N+1));
*(*(s+0)+1)=(char*)malloc(sizeof(char)*(N+1));
*(*(s+0)+2)=(char*)malloc(sizeof(char)*(N+1));

*(*(s+1)+0)=(char*)malloc(sizeof(char)*(N+1));
*(*(s+1)+1)=(char*)malloc(sizeof(char)*(N+1));
*(*(s+1)+2)=(char*)malloc(sizeof(char)*(N+1));

strcpy(*(*(s+0)+0),"ab");
strcpy(*(*(s+0)+1),"cd");
strcpy(*(*(s+0)+2),"ef");

strcpy(*(*(s+1)+0),"gh");
strcpy(*(*(s+1)+1),"ij");
strcpy(*(*(s+1)+2),"kl");

看起来有点复杂,那么对于四星上将和五星上将,我们就不一一展开叙述了。三星上将就足以解决回文串分割这个问题。

  • 有了上面的两小结做铺垫,就可以顺理成章地完成我们程序,本程序采用C语言,而且避免使用动态递归算法。

a.) 头文件 palindrome_partition.h

/**
 * @file palindrome_partition.h
 * @author your name (you@domain.com)
 * @brief 
 * @version 0.1
 * @date 2023-03-02
 * 
 * @copyright Copyright (c) 2023
 * 
 */

#ifndef PALINDROME_PARTITION_H
#define PALINDROME_PARTITION_H
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdbool.h>

/**
 * @brief Find all possible palindrome sub-string array
 * 
 * @param s Original string
 * @param ret_size Number of Sub palindrome string array
 * @param ret_colum_size Number of stiring in each sub palindrome array
 * @return char*** ret
 */
char ***palindrome_partition(char *s,int *ret_size, int **ret_colum_size);

/**
 * @brief Use depth first search to look for all the possible substring that is palindrome
 *
 * @param s Original string
 * @param n Length of string
 * @param ans Answer array in the form of {"a","b","c"}
 * @param ans_size Answer size, the number of string, say {"a","b","c"}=3
 * @param ret Return result in the form of {{"a","b"},{"c","d"}}, the pointer is char ***
 * @param ret_size Total number of palindrome 
 * @param ret_colum_size Return column size
 */
void dfs_palindrome(char *s, int n, int i,char **ans, int *ans_size, char ***ret, int *ret_size, int **ret_column_size);


/**
 * @brief Check whether the substring is palindrome or not
 *
 * @param s Source string
 * @param i Start index
 * @param j End   index
 * @return true if it is a palindrome
 * @return false if it is not a palindrome
*/
bool is_palindrome(char *s, int i, int j);

#endif

b.) 函数定义

/**
 * @file palindrome_partition.c
 * @author your name (you@domain.com)
 * @brief 
 * @version 0.1
 * @date 2023-03-02
 * 
 * @copyright Copyright (c) 2023
 * 
 */
#ifndef PALINDROME_PARTITION_C
#define PALINDROME_PARTITION_C
#include "palindrome_partition.h"

char ***palindrome_partition(char *s, int *ret_size, int **ret_column_size)
{
    int n;
    int max_len;
    char ***ret;
    int ans_size=0;
    int i=0;

    n=strlen(s);
    max_len = n * (1 << n);

    char *ans[n];

    ret=(char ***)malloc(sizeof(char **)*max_len);
    *ret_column_size=(int *)malloc(sizeof(int)*max_len);
    *ret_size=0;

    dfs_palindrome(s, n,i,ans, &ans_size, ret, ret_size, ret_column_size);

    return ret;
}

void dfs_palindrome(char *s, int n, int i, char **ans, int *ans_size, char ***ret, int *ret_size, int **ret_column_size)
{
    int j;
    int len;
    if(i==n)
    {
        char **temp;
        temp=(char**)malloc(sizeof(char *)*(*ans_size));

        for(int k=0;k<(*ans_size);k++)
        {
            *(temp+k)=(char *)malloc(sizeof(char)*(strlen(ans[k])+1));
            strcpy(temp[k],ans[k]);
        }

        *(*ret_column_size+(*ret_size))=*ans_size;
        *(ret+(*ret_size))=temp;
        (*ret_size)+=1;
    }
    else
    {
        //backtracking by using depth first search method
        for(j=i;j<n;j++)
        {
            if(is_palindrome(s,i,j))
            {
                len=j-i+2;
                char    *sub;
                sub=(char*)malloc(sizeof(char)*len);
                memset(sub,0,sizeof(char)*len);
                strncpy(sub,s+i,len-1);
                ans[(*ans_size)++]=sub;
                dfs_palindrome(s,n,j+1,ans,ans_size,ret,ret_size,ret_column_size);
                (*ans_size)--;
            }
        }        
    }
    return;
}

bool is_palindrome(char *s, int i, int j)
{
    while(i<=j)
    {
        if(s[i++]!=s[j--])
        {
            return false;
        }
    }
    
    return true;
}

#endif

c)测试主函数

/**
 * @file palindrome_partition_main.c
 * @author your name (you@domain.com)
 * @brief 
 * @version 0.1
 * @date 2023-03-02
 * 
 * @copyright Copyright (c) 2023
 * 
 */
#ifndef PALINDROME_PARTITION_MAIN_C
#define PALINDROME_PARTITION_MAIN_C
#include "palindrome_partition.c"

int main(void)
{
    char *s="aabab";
    char ***ret;
    int ret_size;
    int *ret_column_size;

    ret_size=0;

    ret = palindrome_partition(s,&ret_size,&ret_column_size);
    printf("The number of palindrome is %d\n",ret_size);

    getchar();
    return EXIT_SUCCESS;
}


#endif

  1. 小结

通过本文,再次对回溯法进行深入理解,其本质就是优先多叉树遍历,遍历过程中,利用数组/栈/线性表对保存的数据进行回溯记录,从而求得新的解。需要关注的是,一旦剪枝条件确认,其深度优先遍历的流程不会受到回溯的任何影响,深度优先遍历只是按照自己的节奏进行层层递推,加上了前序保存和后序撤销前序的保存,便构成了回溯的基本要素。一般都会有循环嵌套递归的形式构成,形成一个多叉递归遍历树。

同时就C语言的多级指针进行了回顾和复习,对一年前的《C on pointers》一书中的指针有了更全新的理解与认识。

参考资料:

  1. 131. 分割回文串 - 力扣(Leetcode)
  2. 《C on pointers》
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值