源码剖析与实践
strtok() 源代码实现
由于是用的static实现的全局变量存储地址,所以只需要传一次,后面传NULL,即可对之前的字符串进行操作。
- 但同样也造成了一个问题:如果有两个线程都用了这一个函数,由于是全局变量,所以他们会共享这个变量,如果两个线程处理的是不同的两个字符串,而它们的地址存储都是用的同一个变量,这会导致原本应该处理的是本身这个线程所处理的字符串而反而去去处理了另一个线程的字符串了。
这就是所谓的线程不安全。
#include<string.h>
static char *_strtok = NULL;
char *
strtok(char *s, char const *ct) {
char *sbegin, *send;
sbegin = s ? s : _strtok;
if (!sbegin) {
return NULL;
}
sbegin += strspn(sbegin, ct);
if (*sbegin == '\0') {
_strtok = NULL;
return (NULL);
}
send = strpbrk(sbegin, ct);
if (send && *send != '\0')
*send++ = '\0';
_strtok = send;
return (sbegin);
}
源代码用的另外几个函数 strspn() 、 strpbrk() 、strcspn()
strspn()
strspn的使用
第二个参数形成一个集合,返回第一个字符串参数中不在集合里的第一个字符下标位置。
#include <stdio.h>
#include <string.h>
int main ()
{
int i;
char strtext[] = "129th";
char cset[] = "1234567890";
i = strspn (strtext,cset);
printf ("The initial number has %d digits.\n",i);
return 0;
}
strspn实现(个人实现版本)
int strspn(const char* s1,const char* s2){
if(s1==NULL || s2==NULL)
return 0;
bool cset[256]={0};
while((*s2)!='\0'){
cset[*s2] = 1;
++s2;
}
int idx = 0;
while(s1[idx] != '\0'){
if(cset[s1[idx]])
idx++;
else
break;
}
return idx;
}
strcspn()
原型:
size_t strcspn(const char *str1, const char *str2)
作用和strspn类似,只不过多了一个c意为constant,连续的。
所以它是找出从str1开始,连续的不在str2字符集合中的长度。
代码实现(个人功能实现)
int strspn(const char* s1,const char* s2){
if(s1==NULL || s2==NULL)
return 0;
bool cset[256]={0};
while((*s2)!='\0'){
cset[*s2] = 1;
++s2;
}
int len = 0;
while(s1[idx] != '\0'){
if(!cset[s1[len]])
len++;
else
break;
}
return len;
}
strpbrk()
strpbrk的使用
同样也是先建立字符的set,返回第一个字符串中在这个集合内的字符的第一个位置的地址(指针)。
#include <stdio.h>
#include <string.h>
int main ()
{
char str[] = "This is a sample string";
char key[] = "aeiou";
char * pch;
printf ("Vowels in '%s': ",str);
pch = strpbrk (str, key);
while (pch != NULL)
{
printf ("%c " , *pch);
pch = strpbrk (pch+1,key);
}
printf ("\n");
return 0;
}
strpbrk的实现(个人实现版本_空间节省版)
为了节省空间我这边将每一个位都利用了,大家如果看不懂,可以用256个bool(一个字节)变量之间存。
typedef unsigned char uchar;
inline int get_pos(uchar x) {
return x % 32;
}
char *strpbrk(const char *s1, const char *s2) {
if (s1 == NULL || s2 == NULL)
return NULL;
uchar cset[32] = {0};
while ((*s2) != '\0') {
uchar t = (uchar) *s2++;
cset[get_pos(t)] |= 1 << (t / 32);
}
while ((*s1) != '\0') {
uchar t = (uchar) *s1;
if (cset[get_pos(t)] & (1 << (t / 32))) {
return (char *) s1;
} else {
++s1;
}
}
return NULL;
}
strtok_r() 源代码实现
函数原型:
char * strtok_r (char *s, const char *delim, char **save_ptr);
由于没有用到static全局变量,所以需要从外面传入一个指针变量用于存储当前操作的字符串。
由于用到的是外部的变量进行缓存,不会产生线程安全问题!
以下为使用测试:
#include <stdio.h>
#include <string.h>
void func() {
char chSrc[] = "Can I help you";
char *save = NULL;
char *pchSrc = chSrc;
while (NULL != (pchSrc = strtok_s(pchSrc, " ", &save))) {
printf("\npchSrc: %s\nsave: %s\n", pchSrc, save);
pchSrc = NULL;
}
}
int main() {
func();
return 0;
}
strtok_r() 源代码:
char *
strtok_r(char *s, const char *delim, char **save_ptr) {
char *end;
if (s == NULL)
s = *save_ptr;
if (*s == '\0') {
*save_ptr = s;
return NULL;
}
s += strspn(s, delim);
if (*s == '\0') {
*save_ptr = s;
return NULL;
}
end = s + strcspn(s, delim);
if (*end == '\0') {
*save_ptr = end;
return s;
}
*end = '\0';
*save_ptr = end + 1;
return s;
}
(不依赖其他函数库)完全自己实现strtok()
设计方案
设计方案:
- 功能:将字符串根据分隔符分割
- 原理:全局变量状态实现缓存
- 关键:由于缺少strspn()和strpbrk(),根据自己实现了strspn()、strcspn()和strpbrk()后,发现就是建立集合进行判断移动指针的过程。
- 为了节省空间我这边将每一个位都利用了,大家如果看不懂,可以用256个bool(一个字节)变量之间存。
还是怕有人看不懂,问为什么需要256个位置进行映射,为什么需要uchar?在这里进行解答:因为一个字符为1个字节,所以如果不考虑符号,则最多表示0~255,由于我们是用数组下标对字符的数值进行映射,所以需要为非负数,故转uchar,而我typedef为uchar只是单纯的觉得unsigned char太长了而已🤣
代码实现
typedef unsigned char uchar;
inline int get_pos(uchar x) {
return x % 32;
}
char *strtok(char *src, const char *delimiters) {
char *sbegin, *send;
static char *ssave = NULL;
sbegin = src ? src : ssave;
uchar cset[32] = {0};
while ((*delimiters) != '\0') {
uchar t = (uchar) *delimiters++;
cset[get_pos(t)] |= 1 << (t / 32);
}
while (*sbegin != '\0' && (cset[get_pos(*sbegin)] & (1 << (((uchar) *sbegin) / 32)))) {
++sbegin;
}
if (*sbegin == '\0') {
ssave = NULL;
return NULL;
}
int idx = 0;
while (sbegin[idx] != '\0' && !(cset[get_pos(sbegin[idx])] & (1 << ((uchar) sbegin[idx] / 32)))) {
++idx;
}
send = sbegin + idx;
if (*send != '\0')
*send++ = '\0';
ssave = send;
return sbegin;
}
性能分析
问题规模N为字符串的长度,由于设计strtok采用的是集合存储方式,而不是暴力枚举,所以时间复杂度占优为 O(n) !而用于存储集合元素的数组也通过位运算实现了空间优化,空间不随问题规模改变,所以 O(1) 的空间复杂度。
时间复杂度:
O
(
N
)
O(N)
O(N)
空间复杂度:
O
(
1
)
O(1)
O(1)
测试用例
测试的main函数
测试的说明:以 ' ' ',' , '.' '-' 为分隔符对整个字符串进行分割。
int main() {
char str[] = "- This 3234d s3242- -d-, a sample string.";
char *pch;
printf("Splitting string \"%s\" into tokens:\n", str);
pch = strtok(str, " ,.-");
while (pch != NULL) {
printf("%s\n", pch);
pch = strtok(NULL, " ,.-");
}
return 0;
}
测试结果:
很好非常perfect!!!
|