本文所有代码均在仓库中,这是一个完整的由纯C语言实现的可以存储任意类型元素的数据结构的工程项目。
最后也是最重要的一点,数据结构的通用性和舒适的体验感,下面以平衡二叉树为例:
#include "tree-structure/balanced-binary-tree/BalancedBinaryTree.h"
#include "tree-structure/balanced-binary-tree/BalancedBinaryTree.h"
int dataCompare(void *, void *);
typedef struct People {
char *name;
int age;
} *People;
int main(int argc, char **argv) {
struct People dataList[] = {
{"张三", 15},
{"李四", 3},
{"王五", 7},
{"赵六", 10},
{"田七", 9},
{"周八", 8},
};
BalancedBinaryTree tree = balancedBinaryTreeConstructor(NULL, 0, dataCompare);
for (int i = 0; i < 6; ++i) {
balancedBinaryTreeInsert(&tree, dataList + i, dataCompare);
}
return 0;
}
/**
* 根据人的年龄比较
*/
int dataCompare(void *data1, void *data2) {
int sub = ((People) data1)->age - ((People) data2)->age;
if (sub > 0) {
return 1;
} else if (sub < 0) {
return -1;
} else {
return 0;
}
}
#include "tree-structure/balanced-binary-tree/BalancedBinaryTree.h"
int dataCompare(void *, void *);
void dataPrint(void *);
typedef struct People {
char *name;
int age;
} *People;
int main(int argc, char **argv) {
struct People dataList[] = {
{"张三", 15},
{"李四", 3},
{"王五", 7},
{"赵六", 10},
{"田七", 9},
{"周八", 8},
};
BalancedBinaryTree tree = balancedBinaryTreeConstructor(NULL, 0, dataCompare);
for (int i = 0; i < 6; ++i) {
balancedBinaryTreeInsert(&tree, dataList + i, dataCompare);
balancedBinaryTreePrint(tree, dataPrint);
printf("-------------\n");
}
return 0;
}
/**
* 根据人的年龄比较
*/
int dataCompare(void *data1, void *data2) {
int sub = ((People) data1)->age - ((People) data2)->age;
if (sub > 0) {
return 1;
} else if (sub < 0) {
return -1;
} else {
return 0;
}
}
/**
* 打印人的年龄
* @param data
*/
void dataPrint(void *data) {
People people = (People) data;
printf("%d", people->age);
}
打印的结果如下:
最后期待大佬们的点赞。
线性表是具有相同类型
n
n
n个数据元素的有限序列:
L
=
(
a
1
,
a
2
,
…
,
a
n
)
L=(a_1,a_2,\dots,a_n)
L=(a1,a2,…,an)
其中
n
n
n为表长,当
n
=
0
n=0
n=0时线性表是一个空表,数据元素在线性表的位置称为位序(从
1
1
1开始)。线性表的性质如下:
使用顺序存储方式实现的线性表称为顺序表。顺序表的特点如下:
struct SequenceList {
void **data;
int length;
int size;
int (*compare)(void *, void *);
};
使用链式存储实现的线性表称为链表。链表由若干个节点连接而成。每个节点包括两部分一部分是存储数据元素的数据域,另一部分是存储其它节点地址的指针域。链表的特点如下:
只有一个指针域的链表称为单链表:
struct SingleLinkedListNode {
void *data;
struct SingleLinkedListNode *next;
};
有两个指针域的链表称为双链表:
struct DoubleLinkedListNode {
void * data;
struct DoubleLinkedListNode *prior;
struct DoubleLinkedListNode *next;
};
把单链表和双链表的第一元素和最后元素就变成了循环链表。
队列是一种只能在表头或表尾进行插入或删除的线性表,它的特点如下:
可以使用顺序存储或链式存储的方式实现队列:
//顺序存储
typedef struct SequenceQueue SequenceQueue;
struct SequenceQueue {
ElementType data[MAX_SIZE];
int front;
int rear;
};
//链式存储
#include "../linkList/SingleLinkedList.h"
typedef struct LinkedQueue LinkedQueue;
struct LinkedQueue {
Node *front;
Node *rear;
};
栈是一种只能在表头或表尾进行插入和删除的线性表。它的特点如下:
可以通过顺序存储和链式存储的方式实现栈:
//顺序存储
typedef struct SequenceStack SequenceStack;
struct SequenceStack {
ElementType data[MAX_SIZE];
int top;
};
//链式存储
typedef struct Node Node, *LinkedStack;
struct Node{
ElementType data;
struct Node * next;
};
串是一个数据元素只能是字符的线性表:
S
=
′
a
1
a
2
…
a
n
′
S='a_1a_2\dots a_n'
S=′a1a2…an′
其中
n
n
n为串长,当
n
=
0
n=0
n=0时串为空串。串中任意连续字符组成的子序列称为该串的子串,包含该子串的串称为主串,不包含串本身的子串称为真子串,空串是任意串的子串。某个字符在串中的序号称为该字符在串中的位置。子串在主串中的位置以子串的第一个字符在主串中的位置来表示。
//String.h
typedef struct String *String;
//String.c
struct String {
char *ch;
int length;
};
子串在串中的定位称为串的模式匹配。通常有以下两种算法:
BF算法也称为简单匹配法,它的算法思想是:将主串中所有与模式串长度相同的子串和模式串对比,直到找到一个完全匹配的子串或所有的子串都不匹配为止。
int BF(String src, String target) {
int i = 1, j = 1;
while (i <= src->length && j <= target->length) {
if (*(src->ch + i - 1) == *(target->ch + j - 1)) {
i++;
j++;
} else {
i = i - j + 2;
j = 1;
}
}
if (j > target->length) {
return i - target->length;
} else {
return 0;
}
}
与BP算法相比KMP算法可以利用匹配失败后的信息,尽量减少模式串与主串的匹配次数以达到快速匹配。首先弄清三个概念:
下面通过以下两个串来描述算法:
//主串:aaabaaaabaa
String src=stringConstructor("aaabaaaabaa");
//模式串:aabaa
String target=stringConstructor("aabaa");
算法开始时先求出模式串中以各个字符为结尾的子串的部分匹配值,并将这些值放到数组partialMarch
中:
//a:0
//aa:1
//aab:0
//aaba:1
//aabaa:2
int partialMarch[]={,0,1,0,1,2};//字符串的位置从1开始,数组下标从0开始,为了便于计算,舍弃数组的第一位
当匹配失败时,主串不再回溯而是保持当前位置不动,模式串也不再从头开始,而是相对于当前位置回退move
位,回退之后继续匹配,move
由以下公式计算:
//回退位数=匹配成功子串的字符个数-匹配成功子串的部分匹配值
//j为模式串当前匹配的位置
move=(j-1)-partialMarch[j-1]
从partialMarch
数组的角度看就是当前字符匹配失败就会找它前一个字符的部分匹配值,因此最后一个字符的部分匹配值将永远用不到,所以可以将partialMarch
数组整体向右移动一个单位得到next
数组(第一个元素以-1
填充):
next[]={,-1,0,1,0,1};
那么move
就变为:
move=(j-1)-next[j]
那么回退后的j
就变为:
j=j-move=next[j]+1
如果将next
数组各个元素加1:
next[]={,0,1,2,1,2};
此时j
为:
j=next[j]
因此最主要的任务就是求解next
数组,原理很简单,设i
为模式串的当前位置,j
为上一位置的next
数组值,即j=next[i-1]
,那么:
i=1
时,next[i]≡0
;i≠1
时:
charAt(target, i) == charAt(target, j)
,那么next[i]=j+1
;charAt(target, i) == charAt(target, j)
,那么就让j=next[j]
,之后继续比较,直至比较相等或j=0
。int *getNext(String target) {
int *next = calloc(target->length + 1, sizeof(int));
*(next + 1) = 0;
int i = 1, j = 0;
while (i < target->length) {
if (j == 0 || charAt(target, i) == charAt(target, j)) {
next[++i] = ++j;
} else {
j = next[j];
}
}
}
// 初始:i=1,j=0
// | i |1|2|3|4|5|
// |target|a|a|b|a|a|
// | next |0| | | | |
// 第一轮:i=1,j=0
// | i |1|2|3|4|5|
// |target|a|a|b|a|b|
// i=2,j=1
// | next |0|1| | | |
// 第二轮:i=2,j=1
// | i |1|2|3|4|5|
// |target|a|a|b|a|b|
// i=3,j=2
// | next |0|1|2| | |
// 第三轮:i=3,j=2
// | i |1|2|3|4|5|
// |target|a|a|b|a|b|
// i=4,j=1
// | next |0|1|2|1| |
//第四轮:i=4,j=1
// | i |1|2|3|4|5|
// |target|a|a|b|a|b|
// i=5,j=2
// | next |0|1|2|1|2|
可以对next
数组进一步优化,当计算出next[++i]
后,如果发现*charAt(target, i) == charAt(target, next[i])
,那么下一次比较必将失败,此时就可以将next[i] = next[next[i]]
。
int *getNextVal(String target) {
int *nextVal = calloc(target->length + 1, sizeof(int));
*(nextVal + 1) = 0;
int i = 1, j = 0;
while (i < target->length) {
if (j == 0 || charAt(target, i) == charAt(target, j)) {
nextVal[++i] = ++j;
if (charAt(target, i) == charAt(target, nextVal[i])) {
nextVal[i] = nextVal[nextVal[i]];
}
} else {
j = nextVal[j];
}
}
}
完整的KMP算法如下:
int enKMP(String src, String target) {
int *next = getNextVal(target);
int i = 1, j = 1;
while (i <= src->length && j <= target->length) {
if (j == 0 || charAt(target, i) == charAt(target, j)) {
i++;
j++;
} else {
j = *(next + j);
}
}
if (j > target->length) {
return i - target->length;
} else {
return 0;
}
}