数据结构与算法学习笔记3
字符串
C++中有string
本文暂讨论C语言中char,下为两种定义方式
-
char *str = “Haha”; // 字符常量区 , 不能重新赋值
str这个指针指向"Haha"这个字符串(位于字符常量区,只能读,不能写),str是指针,占用4个字节,而*str指向字符串的首地址,占用1个字节
*str = “H”;
//报错,*str是常量,常量不可以做左值运算
str++; //str是指针变量,可以,相当于偏移一个char的长度
-
char str[] = “Haha”; // 栈区 , 允许重新赋值
sizeof(str); //大小为5,字符数组,尾部有一个\0存在,所以是5
sizeof(*str); //大小为1,代表首元素的地址,char类型所以是1
str++; //报错,str是数组名字,相当于是常量,不可以进行赋值
-
void fun(int str[])
此时,形参int str[]传参时退化就相当于int *str,所以注意,sizeof(str)其实相当于sizeof(int),不要这样去求字符串的长度。所以在C语言中如果要进行数组的传递,那么一定也要传一个长度,因为后期无法再获得数组的长度,ps:字符串可以(因为字符串末尾有\0,可以作为标志来找长度)
-
只要是" "的字符串,末尾都带/0;但如果是str[]={‘a’,‘b’}这种末尾就不带
-
字符对应的ASCII值范围是0到255(0到127 后面的128到255是拓展的)
-
char类型的范围是-128~127
-
char* a = “hello”;
// a指向字符常量区,不能修改
char b[] = “hello”;// 先将"hello"从字符常量区搬到栈区的b数组中,能修改
void fun(char* c)
// 这种可以传指针也可以传数组,因为数组名字也是指针,传a的话因为a指向字符常量区,所以不能改;传b的话,因为b是一个在栈区的数组,所以可以改
void fun(char c[])// 这种只能传数组,也就是b,数组都不在字符常量区,所以能改
-
子串vs子序列
例字符串ABCDEF,子串需连续(例AB,BCD,DEF,DE这种),子序列不需要连续,相对位置不变OK(比如ADF,当然字串也算子序列)
常见函数
-
拷贝 strcpy
-
连接 strcat
-
比较 strcmp
-
获得长度 strlen
-
检索strstr
-
atoi/itoa
- atoi把字符串变成数字,itoa则相反。
- 例如“-125”变成数字的-125,实际上就是利用’0‘,利用0的ASCII值为48,然后用每个数字,比如1的ASCII值为49,49-48得到数字1,以此类推,itoa与之同理。
题目
1 替换空格
给一个空间足够大的字符串,把字符串内的空格替换为colin
-
分析:因为题干写了空间足够大,所以最好就不要不申请啦
-
方法1:当然还是可以申请的,最暴力的就是申请一个新的,然后遍历依次放入新的串中,如果遇到空格就放colin,然后再继续遍历,直到遍历结束。
-
方法2:不申请的情况下,先计算字符串内有多少空格,根据空格数量计算字符串需要扩张,移动的步数,然后倒序遍历字符串,将字符移位到扩张后的尾部,碰到空格把colin放进去,依次进行重复操作,直到结束。
-
2 单词倒置
- 分析:
- ①利用栈
- ②以空格分割然后拷贝移动(扩成两个内存)
- ③每个单词放到数组的一个元素中,然后利用数组下标进行倒序读取
- ④先整体倒置,词序一样,然后再把每个单词倒置翻转,得到单词倒置结果,只需要遍历两次字符串且没有申请新的空间,算最优方案。
3 字符移位
-
分析:
-
基于空间消耗的方法:
- ①数组空间扩容,然后拷贝一份得到abcdefgabcdefg,从n-k位开始读取n个得到efgabcd
- ②前几个放一个数组,后几个放一个数组,然后利用strcat进行拼接
- ③利用栈,类比于②一样,放到两个栈中,然后依次出栈
- ④队列,abcdefg入队,然后abcd出队后重新入队得到efgabcd
- ⑤环形链表,从相应位置开始读取就行
-
基于时间消耗的方法:
- 可以把前面abcd当成一个单词,efg当成一个单词,所以用上题中④的方案,先整体倒置得到gfedcba,然后前三个倒置,后三个倒置,得到而efgabcd,在原空间进行,没有空间消耗,时间消耗也较少,是最优方案。
4 找出字符串内第一个不重复的字符
- 申请数组,字符作为数组下标,遍历字符串,在对应位置进行元素+1以进行计数,字符串遍历完成且完成数组元素设置后,再次遍历字符串,读取字符对应数组元素的值,第一个计数为1的元素,其数组下标便是所求。
KMP 字符串匹配算法
- 朴素查找算法 (暴力 三个变量 一个标记 两个遍历 找就完了 但时间消耗较高 因此有了KMP空间换时间)
- KMP 字符串匹配算法 可以优化上述方法
前缀 后缀概念
- 例如字符串aabaa,那么 a aa aab aaba 都是该字符串的前缀,a aa baa abaa都是该字符串的后缀,aa就是前缀后缀的最大相同长度
KMP 算法分析
- 如何得到next数组(每个子串前后缀的最大相同长度)?
- ① next[0] = 0
- ② match[i] 和 match[next[i-1]] 进行比较
- 相等 next[i] = next[i-1] + 1
- 不相等
- next[i-1] = 0 时,那么next[i] = 0
- next[i-1] ≠ 0 时,那么match[i] 和 match[next [next[i-1]-1]]进行比较,按照步骤②进行,实际上就是将match[next[i-1]]中的i-1替换成match[next [next[i-1]-1]]中的next[i-1]-1,如此反复循环,直到给next[i]赋值了为止。
- 如何匹配?
- src[i] 和 match [j] 进行比较
- 相等 都++,继续遍历判断
- 不相等
- j = 0,i++
- j ≠ 0,j = next[j-1]
- src[i] 和 match [j] 进行比较
代码
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int *GetNext(const char *match){
int i;
int *pNext = (int*)malloc(sizeof(int)*strlen(match));
int j;
pNext[0] = 0;
i = 1;
j = i - 1;
while(i < strlen(match)){
if(match[i] == match[pNext[j]]){
pNext[i] = pNext[j] + 1;
i++;
}else {
if (pNext[j] == 0) {
pNext[i] = 0;
i++;
}else{
j = pNext[j] - 1;
}
}
}
return pNext;
}
int KMP(const char *src,const char *match){
if (src == NULL || match == NULL)
return -1;
//Next数组计算
int *pNext = NULL;
pNext = GetNext(match);
//匹配
int i = 0;
int j = 0;
while (i < strlen(src) && j < strlen(match)) {
if (src[i] == match[j]) {
i++;
j++;
}else {
if(j == 0){
i++;
}else {
j = pNext[j-1];
}
}
}
if (j == strlen(match)) {
printf("success.\n");
return i - j;
}else {
return -1;
}
}
int main()
{
int nIndex = KMP("weo1abcabcjisaod", "abcabc");
if (nIndex != -1) {
printf("%d\n",nIndex);
}else {
printf("fail.");
}
return 0;
}
Sunday 算法
分析
- 先创建一个0-255的next数组,先赋-1作为数组各元素的初始值。
- 将当前字符在匹配字符串中从右往左第一次出现的位置存到其相应的ASCII对应的下标上(而且无需自己转换,计算机看字符本身就是一个数字,所以字符可以直接作为下标索引使用)。
- 实际上也不需要自己去找从右往左第一次出现的位置,直接从左往右依次赋值给数组即可,后面出现的会覆盖掉前面出现的,也就自动完成了从右往左第一次出现的任务。
代码
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int Sunday(const char *src,const char * match){
if(src == NULL || match == NULL)
return -1;
//获得next数组
int *pNext = NULL;
pNext = (int*)malloc(sizeof(int)*256);
memset(pNext,-1,sizeof(int)*256); //赋初值
int i;
for(i = 0; i < strlen(match); i++){
pNext[match[i]] = i;
//字符当下标 位置存进去
}
//匹配
int j = 0;
int k = 0;
i = 0;
while(i < strlen(src) && j < strlen(match)){
if (src[i] == match[j]) {
i++;
j++;
}else {
if (k + strlen(match) < strlen(src)) {
i = k + strlen(match) - pNext[src[k+strlen(match)]];
k = i;
j = 0;
}else{
return -1;
}
}
}
//判断是否匹配成功
if (j == strlen(match)) {
printf("success.\n");
return k;
}else{
return -1;
}
}
int main(){
int nIndex = Sunday("adhbeiabcabcsudhasi", "abcabc");
if (nIndex == -1) {
printf("fail.\n");
}else{
printf("%d\n",nIndex);
}
return 0;
}
哈希表 HashTable/散列表
- 映射
- 数值和索引有某种关联
哈希表创建
散列函数
- 求整取余法 p=key%N(只是随便举个例)
- 不一定是N 具体情况具体分析
解决哈希冲突的方法
-
开放地址法 (我的位置被人占了 那我去占别人的位置) ①③较为常用
-
① 线性探测(+1+1往后走,走的太慢)
-
② 线性补偿探测(+步长往后走,可能会死循环,所以不常用)
-
③ 线性探测再散列/二次探测(±1,±4,±9,±16,±25……,速度快且避免死循环)
-
④ 随机探测(生成尾随机数直到位置空能放进去位置)
-
开放地址法优化:装载因子
装载因子 = 个数/表长 ;其值越大, 发生冲突的概率越高;如果装载因子>0.8,需要给当前哈希表扩容,以此降低冲突的概率。
扩容过程中涉及到数据迁移的问题,不需要一次性迁移,慢慢移就行,然后搜索的原则是先找新再找旧。
-
-
拉链法 (以链表的形式在同一位置共生,所以指针数组存指向表头的指针,记得赋初值为空)
- 步骤:① 结构体 ②指针数组 ③元素入表(头添加)
- 装载因子可以大于等于1,因为在同一位置共生情况下,表长可以小于原有的元素个数
- 如果链表过长的话,需要折叠,hashmap,红黑树(暂时没学 后续再讨论)
- 分析:
- (优点)
- 处理冲突简单,直接链就行,数据不会造成堆积,不需要依次查找,平均查找长度比较短
- 动态申请,空间申请灵活,不定长也可以处理,减少空间浪费
- 处理数据量巨大时也比较方便,空间浪费显得更少,额外消耗的指针域几乎可以忽略不计
- 增删简单
- (缺点)
- 原始数据占比空间小和数据量小的时候,额外消耗的指针域就显得比开放地址法大,如果把这部分消耗的空间替换为给开放地址法进行数组扩容,那么装载因子会变小,发生冲突的概率下降,平均查找的速度就会提升。
- (优点)
代码(基础版~)
// 哈希表创建及搜索
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct hash{
int nValue;
struct hash *pNext;
}Hash;
Hash **CreateHashTable(int arr[],int nLength){
if (arr == NULL || nLength <= 0)
return NULL;
//表头
Hash **pHash = NULL;
pHash = (Hash**)malloc(sizeof(Hash)*nLength);
memset(pHash, 0, sizeof(Hash*)*nLength); //赋初值
//元素入表
int nIndex;
Hash *pTemp = NULL;
int i;
for (i = 0; i < nLength; i++) {
nIndex = arr[i] % nLength; //获得当前元素所处位置
pTemp = (Hash*)malloc(sizeof(Hash)); //给元素申请空间
pTemp->nValue = arr[i]; //
pTemp->pNext = pHash[nIndex]; //头添加 节点的下一个等于原来的表头
pHash[nIndex] = pTemp; //新来的节点成为新的表头
}
return pHash;
}
//搜索
void HashSearch(Hash **pHash,int nLength,int nNum){
if (pHash == NULL)
return;
int nIndex = nNum % nLength;
Hash *pTemp = pHash[nIndex]; //临时节点
while (pTemp) { //遍历
if (pTemp->nValue == nNum) { //和要找的相同
printf("%d\n",pTemp->nValue); //直接输出
return;
}
pTemp = pTemp->pNext; //不同的话就往下走 继续遍历
}
printf("failed.\n");
}
int main()
{
int arr[] = {10,166,2,18,99,333,15,25,90,376};
Hash **pHash = CreateHashTable(arr, sizeof(arr)/sizeof(arr[0]));
HashSearch(pHash, sizeof(arr)/sizeof(arr[0]), 333);
return 0;
}
- 哈希表的弊端:
- ①空间消耗较大;
- ②内存可能无法一次性加载哈希表全部内容