C语言学习 建议:一定要多手打或者手写代码,不要只看书(因为这样难以应付幽默考试,而且记忆也不深刻
char就是int 为什么说char就是int?如果理解、分辨字符和整型之间的区别?
ASCII码把int和char联系到了一起,通过转义字符“ \ ”,搭起了桥梁。
字符:如 ‘a’, ‘1’ 等,都可以看作十进制的整型(具体值对应ASCII码中)。我们也可以
1 2 int str = '\10'; // 这里就的\10 就是八进制对应的字符,所以对应int的8; int str = 'a' //那么 str储存的就是a对应的int。
总而言之,字符就是int。
typedef的另类使用 参考链接:C/C++ typedef用法详解(真的很详细)-CSDN博客 |
typedef中声明的类型在变量名的位置出现!!于是可以理解函数指针中的定义
typedef
是 C 语言中一个非常有用的关键字,它的主要作用是为现有的数据类型定义一个新的别名。通过 typedef
,可以让代码更具可读性、简洁性和可维护性。以下是 typedef
在 C 语言中的常见运用场景及其详细说明:
1 2 3 4 5 typedef int Integer;typedef float Real;Integer age = 25 ; Real salary = 5000.50 ;
1 2 3 typedef int * IntPtr;IntPtr p1, p2;
1 2 3 typedef int Vector[10 ]; Vector v;
1 2 3 4 5 6 7 typedef struct { int x; int y; } Point; Point p1;
1 2 3 4 5 6 7 8 9 10 11 typedef int (*MathFunc) (int , int ) ; int add (int a, int b) { return a + b; } int main () { MathFunc func = add; printf ("Result: %d\n" , func(2 , 3 )); return 0 ; }
1 2 3 typedef enum { RED, GREEN, BLUE } Color;Color c = RED;
1 2 3 4 5 6 7 8 9 10 11 12 typedef struct { int year; int month; int day; } Date; typedef struct { char name[50 ]; Date birthday; } Person; Person p1;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 #include<stdio.h> int test(int a,int b) { /*do something*/ return 0 } typedef int(*fp)(int,int); int main(void) { fp f1 = test; //表达式1 fp f2 = &test;//表达式2 printf("%p\n",f1); printf("%p\n",f2); return 0; }
在这里,声明了返回类型为int,接受两个int类型参数的函数指针f1和f2,分别给它们进行了赋值。表达式1和表达式2在作用上并没有什么区别。因为函数名在被使用时总是由编译器把它转换为函数指针,而前面加上&不过显式的说明了这一点罢了。
动态内存分配malloc的再探索与形参实参的研究 参考链接:C语言visual studio警告:取消对NULL指针“p”的引用_取消对null指针的引用怎么解决-CSDN博客 |看完这篇文章一定弄懂C语言数组作为函数参数的用法_使用可变长度的数组作为函数参数-CSDN博客
形参和实参:malloc为例 tip: free指针后最好重置指针。
中午突然不明白C语言中的形参和实参在函数中的传递规律,由于free()的使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 void clear_p(int* p) { free(p); p = NULL; // 倘若 没有这一句,我们会发现 } void fun(int* p) { p[0] = p[1]; } int main() { int* p; int n =2; p = (int*)malloc(n * sizeof(int)); if (!p) return 3; p[0] = 0; p[1] = 1; fun(p); printf("%d", p[0]); clear_p(p); printf("%d", p[0]); if (p != NULL) { return 1; } else if (p == NULL) { return 0; } }
经过上面的代码分析,我们可以发现,main 中return 1; printf的两个数分别为1 和 * ;这说明在clear_p中,free改变了传入的参数,而p = NULL并未影响传入的参数。这是为什么呢??
首先,要明确C语言中的参数传递是值传递。当调用clear_p(p)时,传**递的是指针p的值,也就是内存地址的副本。**在函数内部,这个副本被free释放,但main中的p本身并没有被修改,仍然指向原来的地址,此时这个指针变成了悬垂指针(野指针),因为它指向的内存已经被释放。
但是,free操作本身是成功的,因为它释放的是指针所指向的内存块,而不管是通过原始指针还是副本指针。即使传递的是指针的副本,free函数会根据传入的地址来释放对应的内存。因此,main中的p所指向的内存确实被正确释放了。
不过,main中的p变量本身的值(即存储的地址)并没有改变,仍然指向已经被释放的内存区域。这就是为什么在之后检查p != NULL时,条件成立,返回1的原因,因为clear_p函数中的p = NULL只是修改了函数内部的副本,不影响main中的p。
自此,我也了解了函数中形参传递了
那,为何fun(int *p)
也能改变p??,因为我们在调用fun时,传入的参数p实际上是&p[0],也就是地址。也就是说,形参拷贝了一份p[0]地址的副本,形参确实不会改变实参的值(即 p所指向的地址),但我们对其解引用(*p)时,还是对同一个地址操作,那自然会改变实参。
函数介绍:realloc 1 p = (int * ) realloc(p, (n * sizeof(int) ))
这里的realloc为重新分配内存,一般会比原来分配的要大。成功返回新地址,失败返回NULL。
注意,realloc会自动释放原有内存。所以不用free
函数指针 定义 ,这里定义了pf为一个指针,它指向一个返回类型为void,传入参数为char *的函数。
简单使用
1 2 3 4 5 6 7 8 9 void fun1(char *); void fun2(char *); int rount(); void (*pf)(char *); pf = fun1; //正确 pf = rount; // 错误 //下面调用 (*pf)(mis); // 等价于: fun1(mis) pf(mis); //同上
把函数指针作为函数参数
1 2 3 4 void show(void (*fp)(char *), char *str); show(fun1, mis); show(fp, mis);
else
1 2 show(sqrt(4.0));意思是传入sqrt的返回值。 show(sqrt);//传入函数地址
使用示例
1 2 3 4 5 6 7 8 9 10 11 12 typedef int (*func)(ElemType, ElemType); int f_compare(ElemType input, ElemType judge); void LocateElem(func man); int main() { ... func f1 = f_compare; LocateElem(f1); ... return 0; }
字符函数 ctype.h 1 2 3 4 有 isblank() ; //是否为空格、tab。注意,如果为真,则返回一个非0值!!!!,所以如果写if(isblank(a) == TRUE) 是不可取的 isdigit(); //数字 等等等等。 tolower() // 返回小写字母
memset void *memset(void *str, int c, size_t n)
str – 指向要填充的内存区域的指针。
c – 要设置的值,通常是一个无符号字符。
n – 要被设置为该值的字节数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 #include <stdio.h> #include <string.h> // 引入 string.h 头文件以使用 memset int main() { char buffer[10]; // 将 buffer 数组的前5个字节设置为字符 'A',并添加字符串终止符 memset(buffer, 'A', 5); buffer[5] = '\0'; // 确保添加字符串终止符 printf("Buffer after memset: %s\n", buffer); // 将 buffer 数组清零,使用 '\0' 替代 0 memset(buffer, '\0', sizeof(buffer)); // 使用'\0'确保一致性及可读性 printf("Buffer after memset: %s\n", buffer); return 0; }
perror C 库函数 void perror(const char \*str)
把一个描述性错误消息输出到标准错误 stderr。首先输出字符串 str ,后跟一个冒号,然后是一个空格。
字符串函数 strtol——char *转换为long 定义在stdlib.h
long int strtol (const char* str, char** endptr, int base);
str
为要转化的字符串;base
为转化的进制数,为0时默认10进制,0x采用16进制,0采用8进制;endstr
为第一个不能转换的字符指针,为NULL时表示不用这个参数。根据这个可以作连续转换
strtol()
会扫描参数 str 字符串,跳过前面的空白字符 (例如空格,tab缩进等,可以通过 isspace() 函数来检测),直到遇上数字或正负符号才开始做转换,再遇到非数字或字符串结束时(’\0’)结束转换,并将结果返回。如果不能转换或者 str
为空字符串,那么返回 0(0L)
;
1 2 3 4 5 char nm[] = "202 22 2 1 3"; char *p_end; int li1 = strtol(nm, &p_end, 10); // 这里用int是因为win系统下,int long都占4字节 int li2 = strtol(p_end, &p_end, 10); //可以预见的是,li1为202,li2为22; ...
strtok——分割利器 定义在string.h
中
char *strtok(char *str, const char *delim);
str
:要分割的字符串,第一次调用时传入需要分割的字符串,之后传入 NULL 。NULL表示:函数将在同⼀个字符串中被保存的位置开始,查找下⼀个标记。(如果字符串中不存在更多的标记,则返回 NULL 指针。)
delim
:分隔符字符串,用于指定分隔字符串的分隔符集合 。
strtok
函数找到str
中的下⼀个标记,并将其⽤ \0 结尾,返回⼀个指向这个标记的指针。
(注:strtok函数会改变被操作的字符串,所以在使⽤strtok函数切分的字符串⼀般都是临时拷贝的内容并且可修改。)
1 2 3 4 5 6 7 8 9 10 //示例 char arr[] = "[email protected] @.gaoerji"; char* sep = "@."; // @ . 作为两个分割符 char* str = NULL; // 使用 strtok 函数进行字符串分割,每次调用都会返回被分割出的部分 for (str = strtok(arr, sep); str != NULL; str = strtok(NULL, sep)) //巧妙的利用最开始的参数只初始化一次的特性 { // 输出每次分割得到的子字符串 printf("%s\n", str); }
fgets——代替gets 定义在stdio.h
中
char *fgets(char *str, int n, FILE *stream)
fgets会储存换行符(当然,如果你输入超过n,换行符会没地方储存),最大读取为n-1个字符;返回值为指向str的指针,若读取到文件结尾,返回NULL
第三个参数为读取 的文件,这里用stdin从键盘读取,后续会在文件操作中介绍
1 2 3 4 5 6 7 8 9 10 11 12 while(fgets(words, 10, stdin) != NULL && word[0] != '\n') { i = 0; while(words[i] != '\n' && words[i] != '\0') i++; if(words[i] == '\n') // 如果先遇到换行符,换成空字符; 先遇到空字符,else便丢弃多的字符 words[i] == '\0'; else while(getchar() != '\n') //这个操作就是说,一直getchar,知道传入的是‘\n’ continue; //清除缓冲区 puts(words); // 上面一番操作使得str一定没有换行符 }
fputs——puts int fputs(const char *str, FILE *stream)
fputs不会在输入末尾添加换行符,puts会。第二个为要写入 数据的文件(把str puts 到 FILE中),在屏幕上用stdout。
自定义s_gets输入函数 这个函数读取整行输入并用空字符代替换行符。读取n-1个字符,并丢弃剩下的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 char *s_gets(char * st, int n) { char * ret_val; int i = 0; ret_val = fgets(st, n, stdin); if(ret_val) { while(st[i] != '\n' && st[i] != '\0') i++; if(st[i] == '\n') st[i] == '\0'; else while(getchar() != '\n') continue; } return ret_val; }
sprintf——合成字符串 int sprintf(char *str, const char *format, ...)
1 sprinf(str, "%s, %d, %c", stri, 2, c);
strncat——拼接字符串 char *strncat(char *dest, const char *src, size_t n)
dest – 指向目标数组,该数组包含了一个 C 字符串,且足够容纳追加后的字符串,包括额外的空字符。
src – 要追加的字符串。
n – 要追加的最大字符数。
strncpy——复制 char *strncpy(char *dest, const char *src, size_t n)
dest – 指向用于存储复制内容的目标数组。
src – 要复制的字符串。
n – 要从源中复制的字符数。
1 2 3 4 5 6 7 8 char src[40]; char dest[12]; memset(dest, '\0', sizeof(dest)); strcpy(src, "This is runoob.com"); strncpy(dest, src, 10); // desk 最终的目标字符串: This is ru
strncmp——比较 int strncmp(const char *str1, const char *str2, size_t n)
比较前n个字符有么有相同的。
1 if(strncmp(list[i], "astro", 5) == 0)
strcmp 两个字符串相同则返回0
strlen 不计算空字符‘\0’
文件操作✨ 1. 文件定义 参考链接:C语言文件操作超详解(万字解读,细致入微)-CSDN博客 |
FILE
一般都是通过一个FILE的指针来维护这个FILE结构的变量(通过FILE类型的指针找到文件信息区的起始地址),这样使用起来更加方便。
2.文件操作——打开和关闭 fopen() FILE *fopen(const char *filename, const char *mode)
1 2 3 fopen("C:\\", w); //绝对路径 fopen("".\\source\\mode.png")", w)// 这里的.\表示的是当前目录(一般是和.c文件所在的) fopen(""./source/mode.png")", w)// 使用/就是不用搞转义了。
文件使用方式
含义
如果指定文件不存在
“r”(只读)
为了输入数据,打开一个已经存在的文本文件
出错
“w”(只写)
为了输出数据,打开一个文本文件
建立一个新的文件
“a”(追加)
向文本文件尾添加数据
建立一个新的文件
“rb”(只读)
为了输入数据,打开一个二进制文件
出错
“wb”(只写)
为了输出数据,打开一个二进制文件
建立一个新的文件
“ab”(追加)
向一个二进制文件尾添加数据
出错
“r+”(读写)
为了读和写,打开一个文本文件
出错
“w+”(读写)
为了读和写,建一个新的文件
建立一个新的文件
“a+”(读写)
打开一个文件,在文件尾进行读写
建立一个新的文件
“rb+”(读写)
为了读和写打开一个二进制文件
出错
“wb+”(读写)
为了读和写,新建一个新的二进制文件
建立一个新的文件
“ab+”(读写)
打开一个二进制文件,在文件尾进行读和写
建立一个新的文件
注:当使用“w”,“wb”,“w+”,“wb+”打开文件时会清除文件原本存储的数据。
fclose() 该函数的返回值是int类型的:如果关闭成功就返回0值,否则返回EOF(-1)值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 int main() { //打开 FILE* pf = fopen("text.txt","w"); //判断是否打开成功 if (pf == NULL) { perror("fopen"); return 1; } //关闭 if (fclose(pf) == EOF) { //关闭失败 perror("fclose"); return 1; } pf = NULL; return 0; }
rename() int rename(const char *old_filename, const char *new_filename)
如果成功,则返回零。如果错误,则返回 -1,并设置 errno。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 int ret; char oldname[] = "file.txt"; char newname[] = "newfile.txt"; ret = rename(oldname, newname); if(ret == 0) { printf("文件重命名成功"); } else { printf("错误:不能重命名该文件"); }
fflush int fflush(FILE *stream)
如果成功刷新缓冲区,fflush()
返回 0。
如果发生错误,返回 EOF
,并且设置错误标识符(ferror
)。
如果 stream
为 NULL
,则会刷新所有输出流的缓冲区。
如果 stream
是文件指针,则刷新该文件流的输出缓冲区。
3.顺序读写 fputc 当该函数成功运行时返回所存入字符的ASCII码值,否则返回EOF(-1)值。
1 2 3 4 5 6 7 8 FILE* pf = fopen("text.txt", "w"); //写入数据 int i = 'a'; for (i = 0; i < 26; i++) { fputc('a' + i, pf); } //这时候,pf就是字母表:abcde...了
fgetc 该函数成功读入数据时会返回读取字符的ASCII值,反之则会返回EOF(-1)值。
1 2 3 4 5 FILE* pf = fopen("text.txt", "r");//此时读取文件要用"r"的方式打开 while ((i = fgetc(pf)) != EOF) { printf("%c ", i); }
fputs 1 2 fputs("hello", pf); fputs("world", pf);
fgets 该函数只能读取一行的数据
char *fgets(char *str, int n, FILE *stream)
如果成功,该函数返回相同的 str 参数。如果到达文件末尾或者没有读取到任何字符,str 的内容保持不变,并返回一个空指针。
如果发生错误,返回一个空指针。
可以使用连续的fgets读取到两行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 FILE* fp; char str[20]; fp = fopen("file.txt", "w+"); fputs("abc\ndefgn", fp); fclose(fp); fp = fopen("file.txt", "r"); fgets(str, 8, fp); while (fgets(str, 20, fp) == NULL) continue; fputs(str, stdout); fclose(fp); fp = NULL;
fprintf int fprintf(FILE *stream, const char *format, ...)
如果成功,则返回写入的字符总数,否则返回一个负数。
类似sprintf
,不过一个写入对象是文件,一个是字符串
1 fprintf(fp, "%s %s %s %d", "We", "are", "in", 2014);
fscanf int fscanf(FILE *stream, const char *format, ...)
如果成功,该函数返回成功匹配和赋值的个数。如果到达文件末尾或发生读错误,则返回 EOF。
注意,一般而言,fscanf遇到空格便会停止对这个的读取。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 char str1[10], str2[10], str3[10]; int year; FILE * fp; fp = fopen ("file.txt", "w+"); fputs("We are in 2014", fp); rewind(fp); fscanf(fp, "%s %s %s %d", str1, str2, str3, &year); printf("Read String1 |%s|\n", str1 ); // We printf("Read String2 |%s|\n", str2 ); // are printf("Read String3 |%s|\n", str3 ); printf("Read Integer |%d|\n", year ); fclose(fp); return(0); }
fwrite size_t fwrite(const void *ptr, size_t size, size_t nmemb, FILE *stream)
ptr – 这是指向要被写入的元素数组的指针。
size – 这是要被写入的每个元素的大小,以字节为单位。
nmemb – 这是元素的个数,每个元素的大小为 size 字节。
stream – 这是指向 FILE 对象的指针,该 FILE 对象指定了一个输出流。
1 2 3 4 struct S L = { 20,"李四","女" }; FILE* pf = fopen("text.txt", "wb");//二进制输出时用wb fwrite(&L, sizeof(struct S), 1, pf);
就是把L里面的数据,写入到文件中。
fread size_t fread(void *ptr, size_t size, size_t nmemb, FILE *stream)
ptr – 这是指向带有最小尺寸 size*nmemb 字节的内存块的指针。
size – 这是要读取的每个元素的大小,以字节为单位。
nmemb – 这是元素的个数,每个元素的大小为 size 字节。
stream – 这是指向 FILE 对象的指针,该 FILE 对象指定了一个输入流。
1 2 3 4 5 6 struct S s; FILE* pf = fopen("text.txt", "rb");//二进制输出时用rb if (fread(&s, sizeof(struct S), 1, pf) != 0)// 其返回读取的次数 { printf("%d %s %s", s.age, s.name, s.sex); }
sprintf与sscanf 对于sprintf函数它可以将各种数据以各种格式(如%d,%s,%c等等)转换为字符串类型输入到char*类型的str参数中。
对于sscanf函数它可以将字符串类型的str参数的数据以各种格式(如%d,%s,%c等等)输出到各变量中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 struct S { int age; char name[20]; float point; }; int main() { struct S l = { 15,"lisi",88.9f }; char arr[20]; struct S j = { 0 }; //将各类型数据转换为字符串 sprintf(arr, "%d %s %.1f", l.age, l.name, l.point); printf("%s\n", arr); //将字符串数据转换为各种类型 sscanf(arr, "%d %s %f", &(j.age), j.name, &(j.point)); printf("%d %s %.1f", j.age, j.name, j.point); return 0; }
4.随机读写 fseek int fseek(FILE *stream, long int offset, int whence)
该函数运行成功,返回零。否则回非零值。
stream – 这是指向 FILE 对象的指针,该 FILE 对象标识了流。
offset – 这是相对 whence 的偏移量,以字节为单位。
whence – 这是表示开始添加偏移 offset 的位置。它一般指定为下列常量之一:
常量
描述
SEEK_SET
文件的开头
SEEK_CUR
文件指针的当前位置
SEEK_END
文件的末尾
1 2 3 4 5 6 7 FILE* pf = fopen("text.txt", "r"); //设置pf指针从文件起始位置向后偏移3个单位 fseek(pf, 3, SEEK_SET); //读入数据 int i = fgetc(pf); printf("%c", i);
ftell long int ftell(FILE *stream)
成功后,返回位置指示器的当前值。失败时,返回 -1L,并将 errno 设置为系统特定的正值。
1 2 3 4 //设置偏移量 fseek(pf, 0, SEEK_END); //计算偏移量 printf("%d", ftell(pf));
rewind 该函数可以将所传入的文件指针设置指向文件初始位置。
文件缓冲区 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 int main() { //打开 FILE* pf = fopen("text.txt", "wb"); if (pf == NULL) { perror("fopen"); return 1; } //存入 int a = 10000; fwrite(&a, sizeof(int), 1, pf); printf("此20秒数据在文件缓冲区内,打开文件是没有数据的\n"); Sleep(20000);//睡眠10秒 fflush(pf);//此函数可以刷新缓冲区中的数据,使其存入硬盘文件中 printf("此20秒数据从文件缓冲区内读入到文件中,打开文件是有数据的\n"); Sleep(20000); //关闭 fclose(pf); pf = NULL; return 0; }
因为有缓冲区的存在,C语言在操作文件的时候,需要做刷新缓冲区或者在文件操作结束的时候关闭文件。 如果不做,可能导致读写文件的问题。
这也就是说,如果我们不用fflush,如果我们在一个fopen里面写入、然后读取是会失败的。
顺序线性表 定义 1 2 3 4 5 typedef struct { ElemType* elem; int length; //当前长度 int listsize; //当前分配量 }Sqlist;
初始化
1 2 3 4 5 6 void init_list(Sqlist &L){ // C++语法。可以认为,把形参(值传递)变成实参。相当于Sqlist *L。 L.elem = (ElemType *) malloc(LIST_INIT_SIZE * sizeof(ElemType)); if (!L.elem) exit(OVERFLOW); L.length = 0; L. listsize = LIST_INIT_SIZE; }
置零
1 2 3 4 5 Status ClearList_Sq(SqList& L) { L.length = 0; return OK; }
销毁
1 2 3 4 5 6 7 Status DestroyList_Sq(SqList& L) { free(L.elem); L.elem = NULL; L.length=0; L.listsize=0; return OK; }
是否空表
1 2 3 4 5 Status ListEmpty(SqList L) { if (L.length == 0) return TRUE; else return FALSE; }
返回第i个元素
1 2 3 4 5 6 7 8 9 10 Status GetElem_Sq(SqList L, int i, ElemType& e) { if (i<1 || i>L.length) { printf("超过表长"); exit(EXIT_FAILURE); } else e = L.elem[i - 1]; return OK; }
定位一个元素,返回次序
1 2 3 4 5 6 7 8 9 10 11 12 Status LocateElem_Sq(SqList L, ElemType choose, int (*func)(ElemType, ElemType)) { if (L.length == 0) return ERROR; int i; for (i = 0; i < L.length; i++) { if (func(choose, L.elem[i]) == 1) { return i + 1; } } return 0; }
插入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 Status ListInsert_Sq(SqList& L, int i, ElemType e) { if (i<1 || i>L.length) return ERROR; if (L.length > L.listsize) { ElemType* newbase = (ElemType*)realloc(L.elem, (L.listsize + LISTINCREASE) * sizeof(ElemType)); if (!newbase) exit(OVERFLOW); // free(L.elem); L.elem = newbase; L.listsize += LISTINCREASE; } ElemType* q = &(L.elem[i - 1]); for (ElemType* p = &(L.elem[L.length - 1]); p >= q; --p) { *(p + 1) = *p; } *q = e; ++L.length; return OK; }
删除
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 Status ListDelet_Sq(SqList& L, int i, ElemType& e) { if (i<1 || i>L.length) return ERROR; ElemType* p = &(L.elem[i - 1]); e = *p; ElemType* q = L.elem + L.length - 1; //指向最后一位。L.elem为首地址,后面为移动位数 //为什么不写成 = &(L.elem[L.length - 1]) ?? for (++p; p <= q; ++p) { *(p - 1) = *p; } --L.length; return OK; }
线性表赋值★ 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 void Sq_input(SqList& L) { printf("请依次输入顺序线性表的数字,用空格分隔\n"); char* str = (char*)malloc(L.listsize); if (!str) { printf("内存分配失败\n"); return; } if (!fgets(str, L.listsize, stdin)) { printf("读取输入失败\n"); free(str); return; } char* token = NULL; char* endptr = NULL; const char* delimiters = " \t\n"; // 扩展分隔符 int i = 0; token = strtok(str, delimiters); while (token != NULL && i < L.listsize) { long num = strtol(token, &endptr, 10); // 验证转换有效性 if (*endptr != '\0' || endptr == token) { //分析:若强制传入空字符串(如 token = ""),此时:strtol 返回 0,endptr == token(即指向空字符串起始位置)。 //*endptr == '\0',条件 *endptr != '\0' 不成立,会错误接受该 token 为数字 0。 printf("跳过非法输入: %s\n", token); token = strtok(NULL, delimiters); continue; } L.elem[i++] = (int)num; L.length++; token = strtok(NULL, delimiters); } printf("成功输入 %d 个有效数字\n", i); free(str); }
线性链表 单链表 参考链接:【数据结构】链表(单链表实现+详解+原码)-CSDN博客 | 数据结构——图解单链表的初始化、赋值和遍历输出C语言_将链表原始赋值-CSDN博客 |
定义 1 2 3 4 5 6 typedef struct LNode { ElemType data; struct LNode* next; }LNode, *LinkList; // 这里的Lnode并不会有问题,我们也习惯这么做 // 即LNode名称相同。
也就是说,一个LNode(节点),包含两个数据:data和一个指针(指向结构体)。这里定义的LinkList是一个头指针,指向头节点(下图中的phead,当然也可以不用)。
这里我们需要注意,所有的结点在代码中均表现为结构指针,所以我们用 -> 来进行读取和赋值
打印(示例)
1 2 3 4 5 6 7 8 9 10 11 //打印链表 void SListPrint(SListNode* phead) { SListNode* cur = phead;//一般不直接移动头指针,而是创建一个指针变量来移动 while (cur)//当指针为空时结束循环 { printf("%d->", cur->data);//打印该结点的数据 cur = cur->next;//将指针指向下一个结点 } printf("NULL\n"); }
赋值★ 已知数组,插入链表。
1 2 3 4 5 6 7 8 9 10 11 //申请结点 LNode * CreateList(ElemType x) { LinkList newnode = (LinkList)malloc(sizeof(LNode)); if (!newnode) { perror("malloc"); exit(-1); } newnode->data = x; newnode->next = NULL; return newnode; }
头插入
1 2 3 4 5 void List_Push_Front(LinkList& phead, ElemType x) { LinkList newnode = CreateList(x); newnode->next = phead->next; phead ->next = newnode; }
尾插入
1 2 3 4 5 6 7 8 9 10 11 12 13 void List_Pust_Back(LinkList& phead, ElemType x) { LinkList newnode = CreateList(x); if (!phead->next) { phead->next = newnode; } else { LinkList end = phead; while (end->next) { end = end->next; } end->next = newnode; } }// 当然,我们其实可以传入end指针(指向尾结点)
销毁 1 2 3 4 5 6 7 void Delete_List(LinkList& phead) { while (phead) { LinkList next = phead->next; free(phead); phead = next; } }
删除元素——定位链表元素
1 2 3 4 5 6 7 8 9 10 11 12 13 14 Status Delete_elem(LinkList& L, int i) { LinkList p = L; int j = 0; while (j < i - 1 && p ->next) { j++; p = p->next; } if (!(p->next) || j > i - 1) exit(-1); LinkList q = p->next; p->next = q->next; free(q); q = NULL; return OK; }
翻转
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 void reverse_Link(LinkList& phead) { if (!phead) exit(ERROR); LinkList prev = NULL; LinkList next = NULL; LinkList current = phead; current = current->next; while (current) { next = current->next; // 保存下一个节点 current->next = prev; // 反转当前节点的指针 prev = current; // 移动prev到当前节点 current = next; // 移动current到下一个节点 } phead->next = prev; }
静态链表 定义 1 2 3 4 typedef struct{ ElemType data; int cur; //表明其在数组中的位置 }component, SLinkList[MaxSize];
双向链表 定义 1 2 3 4 5 typedef struct DuLNode{ ElemType data; struct DuLNode *prior; struct DuLNode *next; } DuLNode, *DuLinkList;
插入 1 2 3 Status ListInsert(DuLinkList &L, int i, ElemType e){ 略。 }
应用1:一元多项式运算 待完善和补充
栈 顺序栈 定义 栈先进后出
1 2 3 4 5 6 7 8 9 10 typedef struct { int y; // 行 int x; // 列 }ElemType; // 例子,可以改 typedef struct { ElemType* top; ElemType* base; int stacksize; }SqStack;
初始化 1 2 3 4 5 void init_stack(SqStack& T) { T.top = (ElemType*)malloc(MAXSIZE*sizeof(ElemType)); T.base = T.top; T.stacksize = MAXSIZE; }
获取头元素
1 2 3 4 5 int Get_top_elem(SqStack T, ElemType &e) { if (T.base == T.top) return ERROR; e = *(T.top - 1); return OK; }
入栈 1 2 3 4 5 6 7 8 9 10 11 12 13 void push_stack(SqStack& T,int map[][Map_col], ElemType e) { if (T.top - T.base >= T.stacksize) { T.base = (ElemType*)realloc(T.base, (T.stacksize + SIZEADD) * sizeof(ElemType)); if (!T.base) { perror("malloc"); exit(-1); } T.top = T.base + T.stacksize; T.stacksize += SIZEADD; } *T.top++ = e; }
出栈 1 2 3 4 void pop_stack(SqStack& T,int map[][Map_col], ElemType &e) { if (T.base == T.top) exit(-1); e = * --T.top; }
清空栈
1 2 3 4 5 void clear_stack(SqStack& T) { if (T.base != NULL) { T.top = T.base; } }
销毁栈
1 2 3 4 5 6 7 8 void destroy_stack(SqStack& T) { if (T.base != NULL) { // 检查栈是否已初始化 free(T.base); // 释放栈底内存 T.base = NULL; T.top = NULL; T.stacksize = 0; } }
链栈 简单来说,就是单链表,不过,其插入的方式为头插入,即若我输入123456,那么,链表储存的是654321。这样就实现了先进后出。然后每入栈便malloc,出栈便free。
以下 为参考代码
定义
1 2 3 4 5 6 7 8 9 typedef struct StackNode { char data; struct StackNode* next; } StackNode; typedef struct { StackNode* top; int size; } LinkedStack;
入栈 注意,这里的top就是指向了第1个元素,并未安排头结点
1 2 3 4 5 6 7 8 9 10 11 void push(LinkedStack* stack, char value) { StackNode* newNode = (StackNode*)malloc(sizeof(StackNode)); if (!newNode) { printf("内存分配失败\n"); exit(EXIT_FAILURE); } newNode->data = value; newNode->next = stack->top; stack->top = newNode; stack->size++; }
出栈 1 2 3 4 5 6 7 8 9 10 11 12 char pop(LinkedStack* stack) { if (isEmpty(stack)) { printf("栈下溢错误\n"); exit(EXIT_FAILURE); } StackNode* temp = stack->top; char popped = temp->data; stack->top = stack->top->next; free(temp); stack->size--; return popped; }
队列 定义 1 2 3 4 5 6 7 8 9 10 typedef struct QueueNode { BiTNode* treeNode; struct QueueNode* next; } QueueNode; // 队列结构 typedef struct Queue { QueueNode* front; // 队首 QueueNode* rear; // 队尾 } Queue;
初始化 1 2 3 4 // 初始化队列 void initQueue(Queue* q) { q->front = q->rear = NULL; }
入队 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 void enqueue(Queue* q, BiTNode* node) { QueueNode* newNode = (QueueNode*)malloc(sizeof(QueueNode)); if (!newNode) { printf("Memory allocation failed\n"); exit(1); } newNode->treeNode = node; newNode->next = NULL; if (isQueueEmpty(q)) { q->front = q->rear = newNode; } else { q->rear->next = newNode; q->rear = newNode; } }
出队 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 BiTNode* dequeue(Queue* q) { if (isQueueEmpty(q)) { printf("Queue is empty\n"); exit(1); } QueueNode* temp = q->front; BiTNode* treeNode = temp->treeNode; q->front = q->front->next; if (q->front == NULL) { q->rear = NULL; // 如果队列为空,重置队尾指针 } free(temp); // 释放队列节点 return treeNode; }
串 待补充
数组 待补充
二叉树 普通二叉树 定义 1 2 3 4 5 6 typedef int TElemType; typedef struct BiTNode { TElemType data; struct BiTNode* lchild, * rchild; }BiTNode, *BiTree;
完全二叉树建立——队列 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 BiTNode* createNode(TElemType data) { BiTNode* newNode = (BiTNode*)malloc(sizeof(BiTNode)); if (!newNode) { printf("Memory allocation failed\n"); exit(1); } newNode->data = data; newNode->lchild = NULL; newNode->rchild = NULL; return newNode; } BiTree buildCompleteBinaryTree(int arr[], int n) { if (n == 0) return NULL; // 创建根节点 BiTree root = createNode(arr[0]); // 初始化队列 Queue q; initQueue(&q); // 根节点入队 enqueue(&q, root); int i = 1; while (i < n) { BiTNode* current = dequeue(&q); // 添加左子节点 if (i < n) { current->lchild = createNode(arr[i++]); enqueue(&q, current->lchild); } // 添加右子节点 if (i < n) { current->rchild = createNode(arr[i++]); enqueue(&q, current->rchild); } } return root; }
二叉树遍历——递归实现 先序遍历
1 2 3 4 5 6 7 8 9 10 11 12 //先序遍历 void PrevOrder(BTNode* root) { if (root == NULL)//递归终止条件,若遍历到空,则返回 { printf("NULL"); return; } printf("%d", root->data);//先序遍历,先输出根节点的值 PrevOrder(root->left);//递归进入左子树 PrevOrder(root->right);//递归进入右子树 }
中序遍历
1 2 3 4 5 6 7 8 9 10 11 12 //中序遍历 void InOrder(BTNode* root) { if (root == NULL) { printf("NULL"); return; } InOrder(root->left); printf("%d", root->data); // 这里可以换成别的函数,用于遍历操作 InOrder(root->right); }
后序遍历
1 2 3 4 5 6 7 8 9 10 11 12 //后序遍历 void PostOrder(BTNode* root) { if (root == NULL) { printf("NULL"); return; } PostOrder(root->left); PostOrder(root->right); printf("%d", root->data); }
层次遍历——队列实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 void LevelOrder(BTNode* root)//层序遍历按行输出 { Queen q;//创建队列 QueenInit(&q);//队列初始化 int levelsize = 0;//用来统计每层的节点个数 if (root)//根节点不为空 { QueenPush(&q, root);//进入队列 levelsize = 1;//第一层的节点个数为1 } while (!QueenEmpty(&q))//循环条件:队列是否为空 { while (levelsize--)//层节点 { BTNode* front = QueenFront(&q);//取出队头数据 printf("%d ", front->data);//输出队头数据 QueenPop(&q);//因为已经取出数据,删除队头数据,队头数据更新 if (front->left)//左孩子不为空,进入队列 { QueenPush(&q, front->left); } if (front->right)//右孩子不为空,进入队列 { QueenPush(&q, front->right); } } printf("\n");//换行输出 levelsize = QueenSize(&q);//计算队列中的节点数量 } QueenDestroy(&q);//销毁队列 }
非递归遍历——栈 先序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 //先序遍历二叉树 void PreOrder(BiTree T) { SqStack* S; S = InitStack(); BiTreeNode* p; p = T; // 头结点,指向二叉树的根结点 while (p|| !StackEmpty(*S)) { if (p) // 非空 { printf("%c",p->data); Push(S, p); // 入栈 p p = p->LChild; } else // 为空时, 弹出上一个指向的指针 { Pop(S, p); // p接收返回值, 注意,这里栈的值,代表的是指向的节点 p = p->RChild; } } }
中序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 //中序遍历二叉树 void InOrder(BiTree T) { SqStack* S; S = InitStack(); BiTreeNode* p; p = T; while (p || !StackEmpty(*S)) { if (p) { Push(S, p); p = p->LChild; } else { Pop(S, p); printf("%c", p->data); p = p->RChild; } } }
后序
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 void PostOrder(BiTree &T) { // 全篇 SqStack s; InitStack(s); BiTree p = T, r = NULL; // r标记最近访问过的结点 while (p != NULL || !StackEmpty(s)) { if (p != NULL) { push_stack(s, p); // 一直向左走,左孩子入栈 p = p->lchild; } else { // GetTop(s, p); p = *(s.top - 1); // 获取s的栈顶元素赋值给p // GetTop(s,p)意思就是判断栈顶元素的情况 // if (p->rchild && p->rchild != r) { // 若右孩子存在且未被访问 p = p->rchild; // 就让右孩子 push_stack(s, p); // 入栈 p = p->lchild; // 让右孩子向左 //上面三句意思就是让右孩子的左孩子一直入栈,一直向左走 } // else { pop_stack(s, p); // 右孩子为空或未被访问过,就出栈 printf(" %d ",p->data); r = p; // r标记最近访问结点 p = NULL; // r置空 // 置空原因:因为这个结点已经出栈了 //继续指向就没必要了,置空后r不标记任何结点 } } } }
赋值 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 //通过前序遍历的数组"ABD##E#H##CF##G##"构建二叉树 BTNode* BinaryTreeCreate(char* a, int n, int* pi) { if (*pi >= n)//如果索引>=数组的长度,直接返回 return; if (a[*pi] == '#')//如果当前数组的数据是‘#’,则说明此时的节点是NULL,直接返回 { (*pi)++;//索引往后迭代 return NULL; } //因为通过前序遍历数组创建二叉树,所以先创建父节点,再创建它的左、右孩子节点 BTNode* root = BuyBTNode(a[(*pi)++]);//根据当前索引的字符创建父节点,索引迭代 root->left = BinaryTreeCreate(a, n, pi);//递归创建左孩子节点 root->right = BinaryTreeCreate(a, n, pi);//递归创建右孩子节点 return root;//返回创建的父节点 }
销毁 1 2 3 4 5 6 7 8 9 void BinaryTreeDestory(BTNode* root) { if (root == NULL) return; //后序遍历删除节点 BinaryTreeDestory(root->left); BinaryTreeDestory(root->right); free(root); }
计算二叉树结点 1 2 3 4 5 6 int countNodes(BiTree root) { if (root == NULL) { return 0; // 空节点返回 0 } return 1 + countNodes(root->lchild) + countNodes(root->rchild); }
线索二叉树
若结点的左子树为空,则该结点的左孩子指针指向其前驱结点
若结点的右子树为空,则该结点的右孩子指针指向其后继结点
这种指向前驱和后继的指针 称为线索 ,将一棵普通的二叉树以某种次序遍历,并添加线索的过程称为线索化。
定义 1 2 3 4 5 6 typedef enum PointerTag { Link, Thread}; // Link == 0 :指针, Thread == 1:线索 typedef struct BiThrNode{ TElemType data; struct BiThrNode *lchild, *rchild; PointerTag LTag, RTag; }BiThrNode, *BiThrTree;
遍历算法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 Status InOrderTraverse_Thr(BiThrTree T, Status(*Visit)(TElemType e)) { p = T->lchild; while (p != T) { whlie(p->LTag == Link) p = p->lchild; Visit(p->data); whlile(p->RTag == Thread && p->rchild != T) { p = p->rchild; Visit(p->data); } p = p->rchild; } return OK; }
二叉排序树 略,待补充。。。
二叉树的直观打印 参考链接:控制台图形化打印二叉树(c/c++)_c++打印二叉树-CSDN博客 |
bug: 这里改成了二叉树是int类型的,bug是无法正确打印printBiTree中的判断数:有3个,分别代表空格/ \
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 int width, height; void fillArray(BiTree& T, int* a, int i, int j) { int ti = -1; int tj = -1; if (T) //如果该位置有节点 { *(a + (i * width) + j) = T->data; //向数组该点填入字符 if (T->lchild) //有左右孩子给对应的连接线,左右孩子的值在下一层递归赋 { //将该点与其左孩子之间的连线全部填上'/' for (ti = i + 1, tj = j - 1; tj > j - (height - i + 1) / 2; tj--) { *(a + ((ti)*width) + tj) = -99; ti++; } } if (T->rchild) { for (ti = i + 1, tj = j + 1; tj < j + (height - i + 1) / 2; tj++) { *(a + ((ti)*width) + tj) = 99; ti++; } } //经过循环,ti恰好指到其孩子所在的层 fillArray(T->lchild, a, ti, j - (height - i + 1) / 2); fillArray(T->rchild, a, ti, j + (height - i + 1) / 2); } } int DeepTree(BiTree T) { if (!T) { return 0; } return DeepTree(T->lchild) > DeepTree(T->rchild) ? DeepTree(T->lchild) + 1 : DeepTree(T->rchild) + 1; //关键代码:如果该节点的左子树深度大于右子树则将左子树的深度加一返回,这个返回值便是以该节点做根节点的树的深度,右子树深度大时则相反 } void printBiTree(BiTree& T) { int i, j; int n = DeepTree(T); //计算出深度n //在计算机中数据以二进制形式存储,所以一个数据左移1位就相当于乘以2的1次方 width = (2 << n) - 3; // 2^(n+1)-3 height = (2 << (n - 1)) - 1; // 2^n-1 int* a = (int*)malloc(sizeof(int) * (width * height)); // 申请空间 // if (!a) exit(-1); // memset(a, 88, sizeof(a)); if (!a) { perror("malloc"); exit(-1); } // 空间初始化为0 for (i = 0; i < height; i++) { for (j = 0; j < width; j++) { *(a + (i * width) + j) = -88; } } //调用之前定义好的函数,填充打印数组 fillArray(T, a, 0, (width - 1) / 2); //根据打印数组的内容来实现打印 for (i = 0; i < height; i++) { for (j = 0; j < width; j++) { if (*(a + (i * width) + j) == -99) { printf("/"); } else if (*(a + (i * width) + j) == 99) { printf("\\"); } else if (*(a + (i * width) + j) == -88 ) { printf(" "); } else { printf("%d", *(a + (i * width) + j)); } } printf("\n"); } }
哈夫曼树 定义 最优二叉树,带路径的权值总和最小。哈夫曼树中权越大的叶子离根越近。
构造 每次选权值最小的两个结点,构造成一个新树,新数的权值为二者之和。以此类推。
——包含n个叶子结点的哈夫曼树中共有2n-1个结点
——只有度为0或2的结点,没有度为1的结点
算法实现如下:采用顺序存储的结构
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 typedef struct { unsigned int weight; unsigned int parent, lchild, rchild; } HTNode, * HuffmanTree; // 创建哈夫曼树 void CreateHuffmanTree(HuffmanTree* HT, int n, int* weights) { if (n <= 1) return; int m = 2 * n - 1; *HT = (HuffmanTree)malloc((m + 1) * sizeof(HTNode)); HuffmanTree p = *HT; // 初始化 for (int i = 1; i <= m; ++i) { p[i].weight = (i <= n) ? weights[i - 1] : 0; // 编号为1 - n的 权值为概率。其他均为0 p[i].parent = p[i].lchild = p[i].rchild = 0; // 初始化为0 } // 构建哈夫曼树 for (int i = n + 1; i <= m; ++i) { //需要执行 2n-1-n = n-1次。因为共n个初值。 int s1, s2; Select(*HT, i - 1, &s1, &s2); (*HT)[s1].parent = (*HT)[s2].parent = i; // 父母结点指向 (*HT)[i].lchild = s1; // 左右孩子 (*HT)[i].rchild = s2; (*HT)[i].weight = (*HT)[s1].weight + (*HT)[s2].weight; //权值为和 } } // 选择两个最小权重的节点 void Select(HuffmanTree HT, int n, int* s1, int* s2) { // s1,s2为最小值在线性表中的序号 int min1 = 0x7fffffff, min2 = 0x7fffffff; *s1 = *s2 = 0; for (int i = 1; i <= n; ++i) { if (HT[i].parent == 0 && HT[i].weight < min1) { min2 = min1; *s2 = *s1; min1 = HT[i].weight; *s1 = i; } else if (HT[i].parent == 0 && HT[i].weight < min2) { min2 = HT[i].weight; *s2 = i; } } } void fun_task_6() { int weights[] = { 7, 19, 2, 6, 32, 3, 21, 10 }; int n = sizeof(weights) / sizeof(weights[0]); char chars[] = { 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h' }; HuffmanTree HT; HuffmanCode HC; CreateHuffmanTree(&HT, n, weights); HuffmanCoding(HT, &HC, n); PrintHuffmanCode(HC, chars, n); }
哈夫曼编码 代码如下,其他代码为上面的哈夫曼树的构造
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 typedef char** HuffmanCode; // 生成哈夫曼编码 void HuffmanCoding(HuffmanTree HT, HuffmanCode* HC, int n) { char* cd = (char*)malloc(n * sizeof(char)); cd[n - 1] = '\0'; *HC = (HuffmanCode)malloc((n + 1) * sizeof(char*)); for (int i = 1; i <= n; ++i) { int start = n - 1; // 最大编码数量 for (int c = i, f = HT[i].parent; f != 0; c = f, f = HT[f].parent) { if (HT[f].lchild == c) cd[--start] = '0'; //字符为左孩子,则赋0,倒退循环 else cd[--start] = '1'; } (*HC)[i] = (char*)malloc((n - start) * sizeof(char)); strcpy((*HC)[i], &cd[start]); //第i个字符的哈夫曼编码,从start,这样对应前面的start--,方便了赋值 } free(cd); } // 打印哈夫曼编码 void PrintHuffmanCode(HuffmanCode HC, char* chars, int n) { printf("Huffman Codes:\n"); for (int i = 1; i <= n; ++i) { printf("%c: %s\n", chars[i - 1], HC[i]); } }
图(难) 基本 很多,略
构造
邻接矩阵表示
1 2 3 4 5 6 7 8 9 10 11 typedef enum {DG,DN,UDG,UDN} GraphKind; // 四种类型:有向图,有向网... typedef struct ArcCell { int adj; // 在图中,为0表示不相接,为1是相接。 在网中(带权值) 无穷大数(或者0)表示不相接,权值代表相接 infoType *info; //该弧的相关信息 }ArcCell, AdjMatrix[10][10]; // 后面那个便是上面的邻接矩阵10*10的 typedef struct{ AdjMatrix arcs; //邻接矩阵 int vexnum, arcnum; 顶点数和弧数。 GraphKind kind; // 图的种类标志 }
1 2 3 4 5 6 7 for(i=0; i<G.arcnum; i++){ scanf(v1,v2,w); //输入一条边的顶点和权值 i = LocateVex(T,v1); j = LocateVex(T,v2); T.arcs[i][j].adj = w; ... }
关联矩阵表示
x轴表示的是弧,y轴表示的是边。有向图中,-1表示入度。
算法:待日后补充
邻接表 表示(重点)
简单来说就是利用一组链表来表示
有向图的逆邻接表:有向图中对每个结点建立以Vi为头的弧的单链表。就是按照入度来搞
1 2 3 4 5 6 7 8 9 10 11 12 13 typedef struct ArcNdoe { int adjvex; //该弧指向的顶点的位置 struct ArcNode *nextarc; }ArcNode; typedef struct VNode { VertexType data; //顶点的信息,比如上面的 a b c ArcNode *firstarc; // 一个头指针 }VNode, AdjList[10];// 10 为定义的最大结点数 typedef struct { AdjList vertices; int vexnum, arcnum; int kind; }ALGraph;
有向图的十字链表 表示法
水平指针为出度,垂直指针为入度。
1 2 3 4 5 6 7 8 9 10 11 12 typedef struct ArcBox { int tailvex, headvex; // 该弧的头尾 struct ArcBox *hlink, *tlink; //指向与弧头相同和弧尾相同的链 }ArcBox; typedef struct VexNode { VertexType data; ArcBox *firstin, *firstout; //头结点的两个指针 }VexNode; typedef struct { VexNode xlist[10]; int vexnum,arcnum; }
无向图的邻接多重表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 typedef enum {unvisited, visited} VisitIf; typedef struct EBox { VisitIf mark; int ivex,jvex; struct EBox *ilink, *jlink; }EBox; typedef struct VexBox { VertexType data; EBox *firstedge; }VexBox; typedef struct { VexBox adjmulist[10]; int vexnum, edgenum; }
应用示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 // 邻接表节点 typedef struct AdjListNode { int dest; // 目标顶点 struct AdjListNode* next; // 指向下一个邻接点 } AdjListNode; // 邻接表头节点 typedef struct AdjList { int vertexInfo; // 存储顶点信息 AdjListNode* head; // 指向第一个邻接点 } AdjList; // 图结构 typedef struct Graph { int V; // 顶点数 AdjList* array; // 邻接表数组 } Graph; // 创建邻接表节点 AdjListNode* newAdjListNode(int dest) { AdjListNode* newNode = (AdjListNode*)malloc(sizeof(AdjListNode)); newNode->dest = dest; newNode->next = NULL; return newNode; } // 创建图 Graph* createGraph(int V, int* vertexInfos) { Graph* graph = (Graph*)malloc(sizeof(Graph)); graph->V = V; // 创建邻接表数组,并初始化每个链表为空 graph->array = (AdjList*)malloc(V * sizeof(AdjList)); for (int i = 0; i < V; ++i) { graph->array[i].vertexInfo = vertexInfos[i]; // 存储顶点信息 graph->array[i].head = NULL; } return graph; } // 添加边到图 void addEdge(Graph* graph, int src, int dest) { // 添加从src到dest的边 AdjListNode* newNode = newAdjListNode(dest); newNode->next = graph->array[src].head; graph->array[src].head = newNode; } // 打印邻接表 void printGraph(Graph* graph) { for (int v = 0; v < graph->V; ++v) { AdjListNode* pCrawl = graph->array[v].head; printf("\n Adjacency list of vertex %d (info: %d)\n head ", v, graph->array[v].vertexInfo); while (pCrawl) { printf("-> %d", pCrawl->dest); pCrawl = pCrawl->next; } printf("\n"); } } void fun_task_1() { int V, E; // 输入顶点数和顶点信息 printf("Enter number of vertices: "); scanf("%d", &V); int* vertexInfos = (int*)malloc(V * sizeof(int)); printf("Enter vertex information (e.g., 1 2 3 4 5): "); for (int i = 0; i < V; ++i) { scanf("%d", &vertexInfos[i]); } // 输入边数和边信息 printf("Enter number of edges: "); scanf("%d", &E); // 创建图 Graph* graph = createGraph(V, vertexInfos); printf("Enter edges (source destination):\n"); for (int i = 0; i < E; ++i) { int src, dest; scanf("%d %d", &src, &dest); addEdge(graph, src, dest); } // 输出邻接表 printGraph(graph); // 释放动态分配的内存 free(vertexInfos); for (int i = 0; i < V; ++i) { AdjListNode* pCrawl = graph->array[i].head; while (pCrawl) { AdjListNode* temp = pCrawl; pCrawl = pCrawl->next; free(temp); } } free(graph->array); free(graph); }
图的遍历 深度优先搜索 如下图,对应于邻接表而言就是这样。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 bool visited[MAX_VERTEX_NUM]; //访问标记数组 /*从顶点出发,深度优先遍历图G*/ void DFS(Graph G, int v){ int w; visit(v); //访问顶点 visited[v] = TRUE; //设已访问标记 //FirstNeighbor(G,v):求图G中顶点v的第一个邻接点,若有则返回顶点号,否则返回-1。 //NextNeighbor(G,v,w):假设图G中顶点w是顶点v的一个邻接点,返回除w外顶点v for(w = FirstNeighbor(G, v); w>=0; w=NextNeighor(G, v, w)){ if(!visited[w]){ //w为u的尚未访问的邻接顶点 DFS(G, w); } } } /*对图进行深度优先遍历*/ void DFSTraverse(MGraph G){ int v; for(v=0; v<G.vexnum; ++v){ visited[v] = FALSE; //初始化已访问标记数据 } for(v=0; v<G.vexnum; ++v){ //从v=0开始遍历 if(!visited[v]){ DFS(G, v); } } }
下面的代码并不是用的上面的流程图。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 bool DFSUtil(Graph* graph, int v, int target, bool visited[]) { // 标记当前顶点为已访问 visited[v] = true; // 如果找到目标顶点,返回 true if (v == target) { return true; } // 遍历当前顶点的所有邻接顶点 AdjListNode* pCrawl = graph->array[v].head; while (pCrawl) { int adjVertex = pCrawl->dest; if (!visited[adjVertex]) { // 如果邻接顶点未被访问 if (DFSUtil(graph, adjVertex, target, visited)) { return true; // 找到路径 } } pCrawl = pCrawl->next; } return false; // 未找到路径 } // 判断是否存在从 Vi 到 Vj 的路径 bool isPathExist(Graph* graph, int Vi, int Vj) { if (Vi == Vj) { return true; // 起点和终点相同,直接返回 true } // 初始化访问标记数组 bool* visited = (bool*)malloc(graph->V * sizeof(bool)); for (int i = 0; i < graph->V; ++i) { visited[i] = false; } // 调用 DFS 辅助函数 bool result = DFSUtil(graph, Vi, Vj, visited); // 释放动态分配的内存 free(visited); return result; }
广度优先搜索
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 /*邻接矩阵的广度遍历算法*/ void BFSTraverse(MGraph G){ int i, j; Queue Q; for(i = 0; i<G,numVertexes; i++){ visited[i] = FALSE; } InitQueue(&Q); //初始化一辅助用的队列 for(i=0; i<G.numVertexes; i++){ //若是未访问过就处理 if(!visited[i]){ vivited[i] = TRUE; //设置当前访问过 visit(i); //访问顶点 EnQueue(&Q, i); //将此顶点入队列 //若当前队列不为空 while(!QueueEmpty(Q)){ DeQueue(&Q, &i); //顶点i出队列 //FirstNeighbor(G,v):求图G中顶点v的第一个邻接点,若有则返回顶点号,否则返回-1。 //NextNeighbor(G,v,w):假设图G中顶点w是顶点v的一个邻接点,返回除w外顶点v for(j=FirstNeighbor(G, i); j>=0; j=NextNeighbor(G, i, j)){ //检验i的所有邻接点 if(!visited[j]){ visit(j); //访问顶点j visited[j] = TRUE; //访问标记 EnQueue(Q, j); //顶点j入队列 } } } } } }
其他 比如最小生成树、关键路径等,这里略去
最短路径