前言
- 这个专栏将会用纯C实现常用的数据结构和简单的算法;
- 有C基础即可跟着学习,代码均可运行;
- 准备考研的也可跟着写,个人感觉,如果时间充裕,手写一遍比看书、刷题管用很多,这也是本人采用纯C语言实现的原因之一;
- 欢迎收藏 + 关注,本人将会持续更新。
串的定义
串:是一种特殊的线性表,它是由字符组成的,是由零个或多个字符组成的有限序列。一般记为:
S = “a1a2a3……an” (n≥0)
如:A= “BEIJING”, B= “JING”
串相关概念
- 空串(null string):长度为零的串,它不包含任何字符。
- 空格串(black string):任由一个或多个空格组成的串,长度大于等于1
- 子串(sub string):串中任意个连续字符的组成的子序列称为该串的“子串”
- 主串(master string):包含子串的串相应的称为“主串”,因此子串是主串的一部分
- 串长度:当前字符串的元素个数,不包含字符串结束标记\0
- 前缀:除了最后一个字符以外,一个字符串的全部头部组合
- 后缀:除了第一个字符以外,一个字符串的全部尾部组合
串常见操作
串的操作不局限于插入、删除元素,而是各种查找,替换的操作,串的数据对象限制为字符,并且一般以串作为整体进行操作,主要有以下操作:
- 创建串
- 插入子串
- 查找子串
- BF匹配
- KMP匹配
- 删除串
- 区间删除
- 删除子串
- 求串长度
- 串连接
- 串比较
- 串拷贝
串代码的实现
创建串
typedef struct String {char* data;int size;
}String;String* create_string(const char* str)
{if (str == NULL) {return NULL;}int len = strlen(str);String* string = (String*)calloc(1, sizeof(String));assert(string);string->data = (char*)calloc(1, sizeof(char) * len);assert(string->data);for (int i = 0; i < len; i++) {string->data[i] = str[i];}string->size += len;return string;
}
万金油函数
// 万金油函数
bool empty(String* str)
{assert(str);return str->size == 0;
}
int size(String* str)
{assert(str);return str->size;
}
插入
插入方式有很多,我这里实现在pos
位置后插入,实现思路也很多,我这里的思路是:
- 重新申请一块大内存,可以存放插入的字符串;
- 先讲在插入位置后的字符串,全部移到最后面;
- 后一个一个进行插入。
// 向后插入, pos 从0开始
void insert(String* str, int pos, const char* data)
{assert(str);assert(str->data);// 获得插入数据大小int len = strlen(data);// 扩容char* new_data = (char*)realloc(str->data, sizeof(char) * str->size + len);assert(new_data);str->data = new_data; // 保证realloc申请内存失败而导致原来数据为空// 规则:如果pos大于字符串长度,或者无效,则插入末尾if (pos >= str->size || pos < 0) {for (int i = 0; i < len; i++) {str->data[str->size++] = data[i];}}else {// 先腾位置int st = pos + len + 1;int i = pos + 1;for(; st < str->size + len; st++) {str->data[st] = str->data[i++];}for (int i = 0; i < len; i++) {str->data[pos + 1 + i] = data[i];}}str->size += len;
}
删除
我这里也是实现一个简单粗暴的方法,从新省钱内存,将满足条件的字符串一个一个插入:
// 简单方法:重新申请内存, 左闭右开
void erase(String* str, int begin, int end)
{assert(str);int dSzie = end - begin;int count = str->size - dSzie;char* temp = (char*)calloc(count, sizeof(char));assert(temp);int index = 0;for (int i = 0; i < str->size; i++) {if (i >= begin && i < end) {continue;}else {temp[index++] = str->data[i];}}free(str->data);str->data = temp;str->size = count;
}
BF算法与模版
BF算法,即暴力(Brute Force)算法,是普通的模式匹配算法,BF算法的思想就是将目标串S的第一个字符与模式串T的第一个字符进行匹配,若相等,则继续比较S的第二个字符和 T的第二个字符;若不相等,则比较S的第二个字符和T的第一个字符,依次比较下去,直到得出最后的匹配结果,时间复杂度O(n^2)
实现:
int bf_string(String* str1, String* str2)
{assert(str1);assert(str2);int i = 0, j = 0;while (i < str1->size && j < str2->size) {int t = i;if (str1->data[i] == str2->data[j]) {i++;j++;}else {i = t + 1;j = 0;}if (j == str2->size) {return i - j;}}return -1;
}
KMP算法与模版
KMP算法是一种改进的字符串匹配算法,由D.E.Knuth,J.H.Morris和V.R.Pratt提出的,因此人们称它为克努特—莫里斯—普拉特操作(简称KMP算法)。
KMP算法的核心是利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配的目的。
参考资料:[代码随想录KMP](代码随想录 (programmercarl.com))
部分匹配表
KMP
算法确实很绕啊,我感觉一定要结合图像进行学习,才能掌握,这里推荐:
- [代码随想录KMP](代码随想录 (programmercarl.com))
- B站,懒猫老师KMP讲解
我这里讲一下前缀和后缀匹配,方便理解:
- 字符串
ABCDABD
,其实KMP
的next
数组就是代表第i
个字符有多少前缀和最开始字符串相匹配(当然也有前移1位和后移1位,但是本质一样,就是前后缀匹配)。
求解过程:
子串 | 前缀 | 后缀 | 部分匹配值 |
---|---|---|---|
A | 空集 | 空集 | 0 |
AB | A | B | 0 |
ABC | A,AB | C,BC | 0 |
ABCD | A,AB,ABC | D,CD,BCD | 0 |
ABCDA | A,AB,ABC,ABCD | A,DA,CDA,BCDA | 1 |
ABCDAB | A,AB,ABC,ABCD,ABCDA | B,AB,DAB,CDAB,BCDAB | 2 |
ABCDABD | A,AB,ABC,ABCD,ABCDA,ABCDAB | D,BD,ABD,DABD,CDABD,BCDABD | 0 |
代码模版:
void getNext(int next[], String* str)
{assert(str);int j = -1;next[0] = j;for (int i = 1; i < str->size; i++) {while (j >= 0 && str->data[i] != str->data[j + 1]) {j = next[j];}if (str->data[i] == str->data[j + 1]) {j++;}next[i] = j;}
}int kmp_string(String* str1, String* str2)
{assert(str1);assert(str2);int* next = (int*)calloc(str2->size, sizeof(int));assert(next);getNext(next, str2);int j = -1, i = 0;for (; i < str1->size; i++) {if (str1->data[i] == str2->data[j + 1]) {j++;}else if (j >= 0) {j = next[j];}if (j == str2->size - 1) {free(next);return i - j;}}free(next);return -1;
}
总代码
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <assert.h>
#include <string.h>typedef struct String {char* data;int size;
}String;String* create_string(const char* str)
{if (str == NULL) {return NULL;}int len = strlen(str);String* string = (String*)calloc(1, sizeof(String));assert(string);string->data = (char*)calloc(1, sizeof(char) * len);assert(string->data);for (int i = 0; i < len; i++) {string->data[i] = str[i];}string->size += len;return string;
}// 万金油函数
bool empty(String* str)
{assert(str);return str->size == 0;
}
int size(String* str)
{assert(str);return str->size;
}// 向后插入, pos 从0开始
void insert(String* str, int pos, const char* data)
{assert(str);assert(str->data);// 获得插入数据大小int len = strlen(data);// 扩容char* new_data = (char*)realloc(str->data, sizeof(char) * str->size + len);assert(new_data);str->data = new_data; // 保证realloc申请内存失败而导致原来数据为空// 规则:如果pos大于字符串长度,或者无效,则插入末尾if (pos >= str->size || pos < 0) {for (int i = 0; i < len; i++) {str->data[str->size++] = data[i];}}else {// 先腾位置int st = pos + len + 1;int i = pos + 1;for(; st < str->size + len; st++) {str->data[st] = str->data[i++];}for (int i = 0; i < len; i++) {str->data[pos + 1 + i] = data[i];}}str->size += len;
}void print_string(String* str)
{assert(str);for (int i = 0; i < str->size; i++) {printf("%c", str->data[i]);}printf("\n");
}// 简单方法:重新申请内存, 左闭右开
void erase(String* str, int begin, int end)
{assert(str);int dSzie = end - begin;int count = str->size - dSzie;char* temp = (char*)calloc(count, sizeof(char));assert(temp);int index = 0;for (int i = 0; i < str->size; i++) {if (i >= begin && i < end) {continue;}else {temp[index++] = str->data[i];}}free(str->data);str->data = temp;str->size = count;
}int bf_string(String* str1, String* str2)
{assert(str1);assert(str2);int i = 0, j = 0;while (i < str1->size && j < str2->size) {int t = i;if (str1->data[i] == str2->data[j]) {i++;j++;}else {i = t + 1;j = 0;}if (j == str2->size) {return i - j;}}return -1;
}
void erase_bf_string(String* str1, String* str2)
{assert(str1);assert(str2);int begin = bf_string(str1, str2);assert(begin != -1);int end = begin + str2->size;erase(str1, begin, end);
}void getNext(int next[], String* str)
{assert(str);int j = -1;next[0] = j;for (int i = 1; i < str->size; i++) {while (j >= 0 && str->data[i] != str->data[j + 1]) {j = next[j];}if (str->data[i] == str->data[j + 1]) {j++;}next[i] = j;}
}int kmp_string(String* str1, String* str2)
{assert(str1);assert(str2);int* next = (int*)calloc(str2->size, sizeof(int));assert(next);getNext(next, str2);int j = -1, i = 0;for (; i < str1->size; i++) {if (str1->data[i] == str2->data[j + 1]) {j++;}else if (j >= 0) {j = next[j];}if (j == str2->size - 1) {free(next);return i - j;}}free(next);return -1;
}int main()
{String* string = create_string("IamYXZ!!");print_string(string);insert(string, 5, "loveLt");print_string(string);erase(string, 3, 8);print_string(string);String* str2 = create_string("am");printf("%d\n", bf_string(string, str2));erase_bf_string(string, str2);print_string(string);String* str3 = create_string("abcffabcdabcdehf");String* str4 = create_string("abcdabc");printf("%d\n", kmp_string(str3, str4));return 0;
}
KMP,next数组,左移动一位
- 对比的时候以 j + 1与 i 对比
- 除了-1外,下标 j 的值表示的是前 j 的个相同前缀最大数量