线性表
线性表的定义
线性表是具有相同数据类型的n(n>=0)个数据元素的有限序列,其中n为表长,当n=0时线性表是一个空表。线性表中的元素具有逻辑上的顺序性,除了表头和表尾外,每个元素在逻辑上都有一个前驱和后继元素。
。
线性表的顺序表示
线性表的顺序表示是一种将线性表中的元素按照顺序存储在一块连续的存储空间中的方法。在顺序表中,元素的存储位置是连续的,可以通过下标来访问和操作元素。顺序存储结构是一种随机存取的存储结构。
顺序表代码
#define MAX_SIZE 100 // 定义顺序表的最大容量
typedef struct {
int datas[MAX_SIZE]; // 存储数据的数组
int size; // 当前顺序表中的元素个数
} OrderList;
// 在指定位置插入数据
int OrderInsert(OrderList* list, int data, int position) {
if (position < 0 || position > list->size || list->size >= MAX_SIZE) {
return 0; // 插入位置不合法或顺序表已满,插入失败
}
// 将插入位置后的元素依次向后移动一位
for (int i = list->size - 1; i >= position; i--) {
list->datas[i + 1] = list->datas[i];
}
// 在插入位置处插入新的数据
list->datas[position] = data;
list->size++; // 更新顺序表的元素个数
return 1; // 插入成功
}
// 在指定位置插入数据
int OrderInsert(OrderList* list, int data, int position) {
if (position < 0 || position > list->size || list->size >= MAX_SIZE) {
return 0; // 插入位置不合法或顺序表已满,插入失败
}
// 将插入位置后的元素依次向后移动一位
for (int i = list->size - 1; i >= position; i--) {
list->datas[i + 1] = list->datas[i];
}
// 在插入位置处插入新的数据
list->datas[position] = data;
list->size++; // 更新顺序表的元素个数
return 1; // 插入成功
}
线性表的链式存储表示
链表是一种非连续、非顺序的数据结构,由一系列结点组成。每个结点包含两个部分:数据域和指针域。数据域存储结点的数据,指针域存储下一个结点的地址。通过指针将各个结点串联起来,形成链表。
单链表代码
typedef struct ListNode {
int val;
struct ListNode *next;
} ListNode;
// 前插法
ListNode* insertAtHead(ListNode* head, int val) {
ListNode* newNode = (ListNode*)malloc(sizeof(ListNode));
newNode->val = val;
newNode->next = head;
return newNode;
}
// 后插法
也可以完全在函数内输入创建,这样可以定义一个尾指针,比较方便
ListNode* insertAtTail(ListNode* head, int val) {
ListNode* newNode = (ListNode*)malloc(sizeof(ListNode));
newNode->val = val;
newNode->next = NULL;
if (head == NULL) {
return newNode;
}
ListNode* p = head;
while (p->next != NULL) {
p = p->next;
}
p->next = newNode;
return head;
}
ListNode* deleteNode(ListNode* head, int val) {
if (head == NULL) {
return NULL;
}
if (head->val == val) {
ListNode* p = head->next;
free(head);
return p;
}
ListNode* p = head;
while (p->next != NULL && p->next->val != val) {
p = p->next;
}
if (p->next != NULL) {
ListNode* q = p->next;
p->next = q->next;
free(q);
}
return head;
}
//按值查找
ListNode* searchByValue(ListNode* head, int val) {
ListNode* p = head;
while (p != NULL && p->val != val) {
p = p->next;
}
return p;
}
//按位置查找
ListNode* searchByValue(ListNode* head, int val) {
ListNode* p = head;
while (p != NULL && p->val != val) {
p = p->next;
}
return p;
}
顺序表和链表的比较
顺序表是一种基于数组实现的线性表,它的元素在内存中是连续存储的。顺序表的优点是支持随机访问,可以通过下标快速访问任意位置的元素,而且在实现上比较简单。缺点是插入和删除操作比较耗时,需要移动大量元素,尤其是在表中间插入或删除元素时。此外,顺序表的大小是固定的,如果需要动态扩展容量,需要重新分配内存空间,这也会带来一定的开销。
链表是一种基于指针实现的线性表,它的元素在内存中不一定是连续存储的。链表的优点是插入和删除操作比较快速,只需要修改指针即可,而且可以动态扩展容量。缺点是不支持随机访问,需要从头开始遍历链表才能访问任意位置的元素,而且在实现上比较复杂。
适用场景方面,如果需要频繁进行随机访问操作,或者数据量比较小且固定,可以选择顺序表。如果需要频繁进行插入和删除操作,或者数据量比较大且不确定,可以选择链表。
栈和队列
栈
栈是一种线性数据结构,它具有后进先出(LIFO)的特点。栈可以看作是一种只能在一端进行插入和删除操作的特殊线性表。这一端被称为栈顶,另一端被称为栈底。当有新元素插入到栈中时,它就成为了新的栈顶元素,而当元素从栈中弹出时,它就从栈顶被移除。
顺序栈
#include <stdio.h>
#include <stdlib.h>
#define MAXSIZE 100 // 定义栈的最大容量
typedef struct {
int data[MAXSIZE]; // 存储栈中元素
int top; // 栈顶指针
} SqStack;
// 初始化栈
void InitStack(SqStack *S) {
S->top = -1; // 初始化栈顶指针为-1,表示栈为空
}
// 判断栈是否为空
int IsEmpty(SqStack S) {
if (S.top == -1) {
return 1; // 栈为空
} else {
return 0; // 栈不为空
}
}
// 入栈操作
int Push(SqStack *S, int x) {
if (S->top == MAXSIZE - 1) {
return 0; // 栈满,无法入栈
} else {
S->top++; // 栈顶指针加1
S->data[S->top] = x; // 将元素x入栈
return 1; // 入栈成功
}
}
// 出栈操作
int Pop(SqStack *S, int *x) {
if (S->top == -1) {
return 0; // 栈空,无法出栈
} else {
*x = S->data[S->top]; // 将栈顶元素赋值给x
S->top--; // 栈顶指针减1
return 1; // 出栈成功
}
}
int main() {
SqStack S;
int x;
InitStack(&S); // 初始化栈
Push(&S, 1); // 入栈元素1
Push(&S, 2); // 入栈元素2
Push(&S, 3); // 入栈元素3
while (!IsEmpty(S)) { // 当栈不为空时,循环出栈并输出元素
Pop(&S, &x);
printf("%d ", x);
}
return 0;
}
链栈
typedef struct StackNode
{
int data;
struct StackNode *next;
}StackNode, *LinkStack;
//初始化
void InitStack(LinkStack *S)
{
S = NULL:
return ;
}
//入栈
bool push(LinkStack *s, int e)
{
头插法;
}
进制转换:转化为几进制就对几取余,一直把余数压入栈里,最后遍历栈
队列
队列是一种线性数据结构,它具有先进先出(FIFO)的特点。队列可以看作是一个环形的数组,它有两个指针,一个指向队头,一个指向队尾。当元素入队时,它被插入到队尾,当元素出队时,它被删除队头。队列的基本操作包括入队、出队、获取队头元素和获取队列长度等。
顺序队列
定义循环队列结构体
#define MAXSIZE 100 // 循环队列的最大长度
typedef struct {
int data[MAXSIZE]; // 存储队列元素的数组
int front; // 队头指针
int rear; // 队尾指针
} SqQueue;
初始化循环队列
void InitQueue(SqQueue *Q) {
Q->front = Q->rear = 0; // 初始化队头和队尾指针为0
}
判断循环队列是否为空
int QueueEmpty(SqQueue Q) {
if (Q.front == Q.rear) { // 队头和队尾指针相等,说明队列为空
return 1;
} else {
return 0;
}
}
判断循环队列是否已满
int QueueFull(SqQueue Q) {
if ((Q.rear + 1) % MAXSIZE == Q.front) { // 队尾指针加1后等于队头指针,说明队列已满
return 1;
} else {
return 0;
}
}
入队操作
int EnQueue(SqQueue *Q, int x) {
if (QueueFull(*Q)) { // 队列已满,无法入队
return 0;
}
Q->data[Q->rear] = x; // 将元素x插入队尾
Q->rear = (Q->rear + 1) % MAXSIZE; // 队尾指针加1,循环到数组头部
return 1;
}
出队操作
int DeQueue(SqQueue *Q, int *x) {
if (QueueEmpty(*Q)) { // 队列为空,无法出队
return 0;
}
*x = Q->data[Q->front]; // 将队头元素赋值给x
Q->front = (Q->front + 1) % MAXSIZE; // 队头指针加1,循环到数组头部
return 1;
}
求循环队列长度
int QueueLength(SqQueue Q) {
return (Q.rear - Q.front + MAXSIZE) % MAXSIZE; // 队列长度为队尾指针减去队头指针再加上数组长度再取模
}
链队列
#include <stdio.h>
#include <stdlib.h>
// 定义链表节点结构体
typedef struct Node {
int data; // 数据域
struct Node *next; // 指针域
} Node;
// 定义链表队列结构体
typedef struct Queue {
Node *front; // 队头指针
Node *rear; // 队尾指针
int size; // 队列长度
} Queue;
// 初始化队列
void initQueue(Queue *q) {
q->front = q->rear = NULL;
q->size = 0;
}
// 入队操作
void enQueue(Queue *q, int data) {
Node *newNode = (Node*)malloc(sizeof(Node));
newNode->data = data;
newNode->next = NULL;
if (q->rear == NULL) {
q->front = q->rear = newNode;
} else {
q->rear->next = newNode;
q->rear = newNode;
}
q->size++;
}
// 出队操作
int deQueue(Queue *q) {
if (q->front == NULL) {
printf("Queue is empty!\n");
return -1;
}
int data = q->front->data;
Node *temp = q->front;
q->front = q->front->next;
free(temp);
if (q->front == NULL) {
q->rear = NULL;
}
q->size--;
return data;
}
// 求队列长度
int queueSize(Queue *q) {
return q->size;
}
// 测试代码
int main() {
Queue q;
initQueue(&q);
return 0;
}
串和数组
串(String)是由零个或多个字符组成的有限序列,常用于表示文本。串通常被实现为字符数组,即用一个字符类型的数组存储一个串中的字符序列。
kmp
手算next nextval也要掌握
时间复杂度 O ( m + n ) O(m + n) O(m+n)
#include <stdio.h>
#include <string.h>
void get_next(char s[],int next[]);
int KMP(char s1[],char s2[],int next[]);
int main() {
int i= 0;
int next[1000];
char s2[] = "abcac";
char s1[] = "ababcabcacbab";
get_next(s2,next);
i=KMP(s1,s2,next);
printf("%d\n",i);
return 0;
}
void get_next(char s[],int next[])
{
int len=0;
int i=0;
int j=-1;
next[0]=-1;
len=strlen(s);
while(i<len-1)
{
if(j==-1||s[i]==s[j])
{
i++;
j++;
next[i]=j;
}
else
{
j=next[j];
}
}
}
int KMP(char s1[],char s2[],int next[])
{
int i=0;
int j=0;
int len1=strlen(s1);
int len2=strlen(s2);
while(i<len1&&j<len2)
{
if(j==-1||s1[i]==s2[j])
{
i++;
j++;
}
else
{
j=next[j];
}
}
if(j>=len2)
return i-len2+1;
else
return -1;
}
数组
数组是一种数据结构,它由一组相同类型的元素组成,这些元素在内存中是连续存储的。数组可以通过下标来访问其中的元素,下标从0开始,最大下标为数组长度减1。数组的定义包括元素类型和数组长度两部分,例如int arr[10]表示定义了一个包含10个整数元素的数组。
广义表
广义表是一种线性结构,它可以包含其他广义表或者元素。广义表中的元素可以是基本类型,也可以是广义表。广义表可以用递归的方式定义,即一个广义表可以由一个元素和一个广义表组成,或者由多个元素和一个广义表组成。
广义表的表头是指广义表中第一个元素或者子表,而表尾是指除了表头之外的剩余部分。
举个例子,假设有一个广义表 L = ( 1 , ( 2 , 3 ) , ( 4 , ( 5 , 6 ) ) ) L = (1, (2, 3), (4, (5, 6))) L=(1,(2,3),(4,(5,6))), 那么它的表头就是 1,而它的表尾就是 ( ( 2 , 3 ) , ( 4 , ( 5 , 6 ) ) ) ((2, 3), (4, (5, 6))) ((2,3),(4,(5,6)))。同样地, ( ( 2 , 3 ) , ( 4 , ( 5 , 6 ) ) ) ((2, 3), (4, (5, 6))) ((2,3),(4,(5,6))) 的表头是 ( 2 , 3 ) (2, 3) (2,3),而它的表尾是 ( ( 4 , ( 5 , 6 ) ) ) ((4, (5, 6))) ((4,(5,6)))。
树和二叉树
树是一种非线性数据结构,它由若干个节点和连接这些节点的边组成。树的定义如下:
树是一个有n(n>=0)个节点的有限集合,其中:
- 有且仅有一个节点没有父节点,该节点称为根节点。
- 其余节点都有且仅有一个父节点。
- 每个节点可以有任意多个子节点。
树的基本术语包括:
- 结点的度:结点拥有的子树的数量
- 树的度:树內各节点度的最大值
- 叶子:度为0的结点称为叶子或者终端结点
- 非终端结点:度不为0的结点称为非终端结点或分支结点,除了根节点以外,非终端节点也称为内部结点
- 层次:结点的层次从根节点开始定义,根为第一层,根的孩子为第二层
- 树的深度:树中结点的最大层次称为树的深度或者高度
二叉树
二叉树是一种树形结构,它的每个节点最多只有两个子节点,分别称为左子节点和右子节点。
二叉树代码
定义,遍历(先中后层次),复制,深度,统计结点个数
#include <stdio.h>
#include <stdlib.h>
// 定义二叉树结点
typedef struct TreeNode {
int val;
struct TreeNode *left;
struct TreeNode *right;
} TreeNode;
// 先序遍历
void preorderTraversal(TreeNode* root) {
if (root == NULL) return;
printf("%d ", root->val);
preorderTraversal(root->left);
preorderTraversal(root->right);
}
// 中序遍历
void inorderTraversal(TreeNode* root) {
if (root == NULL) return;
inorderTraversal(root->left);
printf("%d ", root->val);
inorderTraversal(root->right);
}
// 后序遍历
void postorderTraversal(TreeNode* root) {
if (root == NULL) return;
postorderTraversal(root->left);
postorderTraversal(root->right);
printf("%d ", root->val);
}
// 先序遍历创建二叉树
TreeNode* createTreeByPreorder() {
int val;
scanf("%d", &val);
if (val == -1) return NULL; // -1 表示空结点
TreeNode* root = (TreeNode*)malloc(sizeof(TreeNode));
root->val = val;
root->left = createTreeByPreorder();
root->right = createTreeByPreorder();
return root;
}
// 计算二叉树深度
int maxDepth(TreeNode* root) {
if (root == NULL) return 0;
int leftDepth = maxDepth(root->left);
int rightDepth = maxDepth(root->right);
return leftDepth > rightDepth ? leftDepth + 1 : rightDepth + 1;
}
// 计算二叉树结点个数
int countNodes(TreeNode* root) {
if (root == NULL) return 0;
int leftCount = countNodes(root->left);
int rightCount = countNodes(root->right);
return leftCount + rightCount + 1;
}
int main() {
// 创建二叉树
TreeNode* root = createTreeByPreorder();
// 遍历二叉树
printf("Preorder Traversal: ");
preorderTraversal(root);
printf("\n");
printf("Inorder Traversal: ");
inorderTraversal(root);
printf("\n");
printf("Postorder Traversal: ");
postorderTraversal(root);
printf("\n");
// 复制二叉树
TreeNode* newRoot = copyTree(root);
// 计算二叉树深度和结点个数
int depth = maxDepth(root);
int count = countNodes(root);
printf("Depth: %d, Count: %d\n", depth, count);
return 0;
}
线索二叉树
线索二叉树是一种特殊的二叉树,它的每个节点都有两个指针,如果某个节点没有左子树,则将其左子树指针指向该节点在中序遍历中的前驱节点;如果某个节点没有右子树,则将其右子树指针指向该节点在中序遍历中的后继节点。
线索二叉树的主要作用是加速二叉树的遍历操作。在普通的二叉树中,为了遍历整棵树,需要使用递归或者栈等数据结构来保存遍历过程中的节点。而在线索二叉树中,由于每个节点都有前驱和后继线索,因此可以直接通过这些线索来遍历整棵树,而不需要使用额外的数据结构。
线索二叉树代码
定义:
typedef struct ThreadNode {
int data;
struct ThreadNode *left, *right;
int ltag, rtag; // 0表示指向子节点,1表示指向前驱或后继节点
} ThreadNode, *ThreadTree;
构造:
void InThread(ThreadTree p, ThreadTree *pre) {
if (p != NULL) {
InThread(p->left, pre);
if (p->left == NULL) {
p->left = *pre;
p->ltag = 1;
}
if (*pre != NULL && (*pre)->right == NULL) {
(*pre)->right = p;
(*pre)->rtag = 1;
}
*pre = p;
InThread(p->right, pre);
}
}
void CreateInThread(ThreadTree root) {
ThreadTree pre = NULL;
if (root != NULL) {
InThread(root, &pre);
pre->right = NULL;
pre->rtag = 1;
}
}
遍历:
ThreadTree FirstNode(ThreadTree p) {
while (p->ltag == 0) {
p = p->left;
}
return p;
}
ThreadTree NextNode(ThreadTree p) {
if (p->rtag == 0) {
return FirstNode(p->right);
} else {
return p->right;
}
}
void InOrder(ThreadTree root) {
for (ThreadTree p = FirstNode(root); p != NULL; p = NextNode(p)) {
printf("%d ", p->data);
}
}
以上是线索二叉树的代码实现,其中InThread函数用于构造线索二叉树,CreateInThread函数用于创建线索二叉树,FirstNode和NextNode函数用于遍历线索二叉树,InOrder函数用于中序遍历线索二叉树。
树和森林
树的存储结构
树的存储方式有三种:双亲表示法、孩子表示法和孩子兄弟表示法。
-
双亲表示法:
双亲表示法是一种顺序存储结构,它用一个一维数组来存储树中的所有节点。对于每个节点,数组中存储它的双亲节点的下标。根节点的双亲节点下标为-1。这种存储方式易于实现,但是查找某个节点的双亲节点需要遍历整个数组。 -
孩子表示法:
孩子表示法是一种链式存储结构,它用一个一维数组来存储树中的所有节点。对于每个节点,数组中存储它的第一个孩子节点的下标。如果该节点没有孩子,则存储-1。这种存储方式易于查找某个节点的孩子节点,但是查找某个节点的兄弟节点需要遍历整个数组。 -
孩子兄弟表示法:
孩子兄弟表示法也是一种链式存储结构,它用一个二叉树来存储树中的所有节点。对于每个节点,二叉树的左子树指向它的第一个孩子节点,右子树指向它的下一个兄弟节点。这种存储方式既可以查找某个节点的孩子节点,也可以查找某个节点的兄弟节点,但是需要额外的指针来表示树的根节点。
哈夫曼树
哈夫曼树是一种带权路径长度最短的树,也称为最优二叉树。它的构造方法是通过贪心算法,将权值较小的节点放在离根节点较远的位置,权值较大的节点放在离根节点较近的位置,从而使得整棵树的带权路径长度最小。
具体构造方法如下:
- 将所有节点按照权值从小到大排序。
- 取出权值最小的两个节点作为左右子节点,构造一棵新的二叉树,根节点的权值为左右子节点权值之和。
- 将新构造的二叉树插入到原来的节点序列中,并删除原来的两个节点。
- 重复步骤2和3,直到只剩下一个节点为止,这个节点就是哈夫曼树的根节点。
需要注意的是,哈夫曼树的构造方法并不唯一,但是它们都能保证带权路径长度最小。
#include <stdio.h>
#include <stdlib.h>
#define MAX 1000
// 定义哈夫曼树节点结构体
typedef struct {
int weight; // 权值
int parent; // 父节点下标
int lchild; // 左孩子下标
int rchild; // 右孩子下标
} HTNode, *HuffmanTree;
// 定义哈夫曼编码结构体
typedef struct {
char ch; // 字符
char code[MAX]; // 编码
} CodeNode, *HuffmanCode;
// 选择两个权值最小的节点
void select(HuffmanTree HT, int n, int *s1, int *s2) {
int i;
int min1 = MAX, min2 = MAX;
for (i = 1; i <= n; i++) {
if (HT[i].parent == 0) { // 如果该节点没有父节点
if (HT[i].weight < min1) { // 如果该节点权值小于最小值1
min2 = min1;
*s2 = *s1;
min1 = HT[i].weight;
*s1 = i;
} else if (HT[i].weight < min2) { // 如果该节点权值小于最小值2
min2 = HT[i].weight;
*s2 = i;
}
}
}
}
// 构建哈夫曼树
void createHuffmanTree(HuffmanTree *HT, int n) {
if (n <= 1) {
return;
}
int m = 2 * n - 1; // 哈夫曼树的节点数
*HT = (HuffmanTree) malloc((m + 1) * sizeof(HTNode)); // 动态分配哈夫曼树空间
int i;
for (i = 1; i <= m; i++) { // 初始化哈夫曼树
(*HT)[i].parent = 0;
(*HT)[i].lchild = 0;
(*HT)[i].rchild = 0;
}
for (i = 1; i <= n; i++) { // 输入n个叶子节点的权值
scanf("%d", &(*HT)[i].weight);
}
int s1, s2;
for (i = n + 1; i <= m; i++) { // 构建哈夫曼树
select(*HT, i - 1, &s1, &s2); // 选择两个权值最小的节点
(*HT)[s1].parent = i;
(*HT)[s2].parent = i;
(*HT)[i].lchild = s1;
(*HT)[i].rchild = s2;
(*HT)[i].weight = (*HT)[s1].weight + (*HT)[s2].weight;
}
}
// 构建哈夫曼编码
void createHuffmanCode(HuffmanTree HT, HuffmanCode *HC, int n) {
*HC = (HuffmanCode) malloc((n + 1) * sizeof(CodeNode)); // 动态分配哈夫曼编码空间
int i;
for (i = 1; i <= n; i++) { // 初始化哈夫曼编码
(*HC)[i].ch = i;
(*HC)[i].code[0] = '\0';
}
char *code = (char *) malloc(n * sizeof(char)); // 分配临时存储编码的空间
code[n - 1] = '\0'; // 编码结束符
for (i = 1; i <= n; i++) { // 构建哈夫曼编码
int start = n - 1; // 编码起始位置
int c = i; // 当前节点下标
int f = HT[c].parent; // 当前节点的父节点下标
while (f != 0) { // 如果当前节点有父节点
if (HT[f].lchild == c) { // 如果当前节点是父节点的左孩子
code[--start] = '0'; // 编码为0
} else { // 如果当前节点是父节点的右孩子
code[--start] = '1'; // 编码为1
}
c = f;
f = HT[c].parent;
}
strcpy((*HC)[i].code, &code[start]); // 将编码复制到哈夫曼编码中
}
free(code); // 释放临时存储编码的空间
}
// 主函数
int main() {
HuffmanTree HT;
HuffmanCode HC;
int n;
printf("请输入叶子节点个数:");
scanf("%d", &n);
printf("请输入%d个叶子节点的权值:", n);
createHuffmanTree(&HT, n); // 构建哈夫曼树
createHuffmanCode(HT, &HC, n); // 构建哈夫曼编码
printf("哈夫曼编码如下:\n");
int i;
for (i = 1; i <= n; i++) { // 输出哈夫曼编码
printf("%c:%s\n", HC[i].ch, HC[i].code);
}
return 0;
}
图
以下是一些图的基本术语:
-
无向完全图/有向完全图:对于无向图,若有 n ( n − 1 ) / 2 n(n-1)/2 n(n−1)/2 条边,则称为无向完全图,对于有向图,若有 n ( n − 1 ) n(n-1) n(n−1)条弧,则称为有向完全图
-
度、入度、出度:
顶点 V V V的度是指和 V V V相关联的边的数量
对于有向图,顶点 V V V的度分为入度和出度,入度是以顶点 V V V为头的弧的数量,出度是以顶点 V V V为尾的弧的数量
-
简单路径、简单回路、简单环:序列中顶点不重复出现的路径称为简单路径,除了第一个顶点和最后一个顶点之外,其他顶点不重复出现的回路,称为简单回路或者简单环
-
连通分量:无向图中极大连通子图,有向图中的极大强联通子图称作强连通分量
-
连通图的生成树:极小连通子树,一颗有n个顶点的生成树有且仅有n-1条边,多一条边和少一条边都不行
图的存储结构
邻接表是一种链式存储结构,它将每个节点的所有邻居节点都存储在一个链表中。具体来说,对于每个节点,我们可以使用一个链表来存储它的所有邻居节点。
邻接表的优点是可以节省空间,因为只有实际存在的边才会被存储。此外,邻接表可以很容易地找到一个节点的所有邻居节点,因为它们都存储在同一个链表中。
但是,邻接表的缺点是在查找两个节点之间是否存在一条边时需要遍历链表,因此时间复杂度较高。
邻接矩阵是一种二维数组,其中行和列分别表示图中的节点。如果两个节点之间存在一条边,则在相应的行和列上标记为1,否则标记为0。
邻接矩阵的优点是可以很容易地查找两个节点之间是否存在一条边,因为只需要查找相应的行和列即可。此外,邻接矩阵可以很容易地进行矩阵运算,例如计算两个节点之间的最短路径。
但是,邻接矩阵的缺点是需要占用大量的空间,因为即使两个节点之间不存在边,相应的位置也需要用0填充。
综上所述,邻接表适用于稀疏图,而邻接矩阵适用于稠密图。在实际应用中,我们需要根据具体情况选择合适的存储方式。
邻接表:
#include <stdio.h>
#include <stdlib.h>
#define MAX_VERTEX_NUM 100 // 最大顶点数
// 边表结点
typedef struct ArcNode {
int adjvex; // 邻接点在顶点数组中的下标
struct ArcNode *next; // 指向下一个邻接点的指针
} ArcNode;
// 顶点表结点
typedef struct VNode {
int data; // 顶点数据
ArcNode *firstarc; // 指向第一个邻接点的指针
} VNode, AdjList[MAX_VERTEX_NUM];
// 图
typedef struct {
AdjList vertices; // 邻接表
int vexnum, arcnum; // 顶点数和边数
} ALGraph;
// 创建邻接表
void CreateALGraph(ALGraph *G) {
printf("请输入顶点数和边数:");
scanf("%d%d", &G->vexnum, &G->arcnum);
printf("请输入顶点数据:");
for (int i = 0; i < G->vexnum; i++) {
scanf("%d", &G->vertices[i].data); G->vertices[i].firstarc = NULL;
}
printf("请输入边的信息(起点 终点):\n");
for (int i = 0; i < G->arcnum; i++) {
int v1, v2;
scanf("%d%d", &v1, &v2);
ArcNode *p = (ArcNode *)malloc(sizeof(ArcNode));
p->adjvex = v2;
p->next = G->vertices[v1].firstarc;
G->vertices[v1].firstarc = p;
}
}
// 打印邻接表
void PrintALGraph(ALGraph G) {
printf("邻接表:\n");
for (int i = 0; i < G.vexnum; i++) {
printf("%d -> ", G.vertices[i].data);
ArcNode *p = G.vertices[i].firstarc;
while (p != NULL) {
printf("%d ", G.vertices[p->adjvex].data);
p = p->next;
}
printf("\n");
}
}
int main() {
ALGraph G;
CreateALGraph(&G);
PrintALGraph(G);
return 0;
}
邻接矩阵:
#include <stdio.h>
#include <stdlib.h>
#define MAX_VERTEX_NUM 100 // 最大顶点数
// 图
typedef struct {
int vertices[MAX_VERTEX_NUM][MAX_VERTEX_NUM]; // 邻接矩阵
int vexnum, arcnum; // 顶点数和边数
} MGraph;
// 创建邻接矩阵
void CreateMGraph(MGraph *G) {
printf("请输入顶点数和边数:");
scanf("%d%d", &G->vexnum, &G->arcnum);
printf("请输入顶点数据:");
for (int i = 0; i < G->vexnum; i++) {
for (int j = 0; j < G->vexnum; j++) {
G->vertices[i][j] = 0;
}
}
for (int i = 0; i < G->vexnum; i++) {
int data;
scanf("%d", &data);
}
printf("请输入边的信息(起点 终点):\n");
for (int i = 0; i < G->arcnum; i++) {
int v1, v2;
scanf("%d%d", &v1, &v2);
G->vertices[v1][v2] = 1;
}
}
// 打印邻接矩阵
void PrintMGraph(MGraph G) {
printf("邻接矩阵:\n");
for (int i = 0; i < G.vexnum; i++) {
for (int j = 0; j < G.vexnum; j++) {
printf("%d ", G.vertices[i][j]);
}
printf("\n");
}
}
int main() {
MGraph G;
CreateMGraph(&G);
PrintMGraph(G);
return 0;
}
图的遍历
深度优先搜索
深度优先搜索(Depth-First Search,DFS)是一种用于遍历或搜索树或图的算法。它从根节点开始,沿着一条路径直到无法继续为止,然后回溯到前一个节点,尝试另一条路径,直到所有节点都被访问为止。
具体来说,深度优先搜索算法可以描述为:
- 从起始节点开始遍历。
- 如果当前节点未被访问过,则将其标记为已访问。
- 对于当前节点的每个未访问过的邻居节点,递归地执行步骤2和步骤3。
- 当所有邻居节点都被访问过后,回溯到前一个节点,重复步骤3。
广度优先搜索
广度优先搜索(BFS)是一种图形搜索算法,它从图的起始点开始遍历,先访问起始点的所有邻居节点,然后再依次访问每个邻居节点的邻居节点,直到遍历完整张图。BFS通常使用队列来实现,每次将当前节点的所有未访问邻居节点加入队列中,然后从队列中取出一个节点作为当前节点,继续遍历其邻居节点。BFS可以用于解决最短路径问题,因为它保证了在遍历到目标节点时,已经访问过的节点都是离起始点最近的节点。
BFS算法的描述如下:
- 将起始节点加入队列中,并标记为已访问。
- 从队列中取出一个节点作为当前节点。
- 遍历当前节点的所有未访问邻居节点,将它们加入队列中,并标记为已访问。
- 如果队列不为空,则重复步骤2-3,直到队列为空或者找到目标节点。
最小生成树
普里姆算法
普里姆算法的基本思想是从一个顶点开始,每次选择一个与当前生成树相邻的权值最小的边,将其加入生成树中,直到生成一棵包含所有顶点的最小生成树。具体过程如下:
- 任选一个起始顶点,将其加入生成树中。
- 找到与当前生成树相邻的所有边,并选择其中权值最小的一条边。
- 将该边加入生成树中,并将该边所连接的顶点加入已访问的顶点集合中。
- 重复步骤2和3,直到所有顶点都被访问过。
克鲁斯卡尔算法
克鲁斯卡尔算法的基本思想是先将所有边按照权值从小到大排序,然后依次选择每条边,如果该边的两个端点不在同一个连通块中,则将其加入生成树中,并将这两个端点所在的连通块合并。具体过程如下:
- 将所有边按照权值从小到大排序。
- 依次选择每条边,如果该边的两个端点不在同一个连通块中,则将其加入生成树中,并将这两个端点所在的连通块合并。
- 重复步骤2,直到生成一棵包含所有顶点的最小生成树。
最短路径
迪杰斯特拉算法
迪杰斯特拉算法的具体步骤如下:
- 初始化:将源点加入到集合S中,dist数组中将源点的距离设为0,visited数组中将源点标记为已访问。
- 从剩余的顶点中选择一个距离源点最近的顶点u,并将其加入到集合S中。
- 对于所有从u出发的边,更新其它顶点到源点的距离。具体地,对于每个与u相邻的顶点v,如果v未被访问过且从源点到v的距离比当前记录的距离更短,则更新dist[v]的值为dist[u]+graph[u][v]。
- 重复步骤2和步骤3,直到所有顶点都被加入到集合S中。
最终,dist数组中记录的就是源点到各个顶点的最短距离。
弗洛伊德算法
弗洛伊德算法(Floyd)是一种用于寻找给定的加权图中多源点之间最短路径的算法。它是一种动态规划算法,通过逐步地求解子问题来求解整个问题。下面是算法的描述:
- 初始化:对于每一对顶点 i 和 j,如果存在边从 i 到 j,则将其权重设为 w(i,j),否则将其权重设为无穷大。
- 对于每一个顶点 k,依次考虑所有顶点对 (i,j),如果从 i 到 j 的最短路径上必须经过顶点 k,则更新最短路径的权重:w(i,j) = min(w(i,j), w(i,k) + w(k,j))。
下面是C语言实现的代码:
#include <stdio.h>
#define INF 99999
void floyd(int graph[][4], int n) {
int dist[n][n];
int i, j, k;
// 初始化
for (i = 0; i < n; i++) {
for (j = 0; j < n; j++) {
dist[i][j] = graph[i][j];
}
}
// 逐步求解子问题
for (k = 0; k < n; k++) {
for (i = 0; i < n; i++) {
for (j = 0; j < n; j++) {
if (dist[i][k] + dist[k][j] < dist[i][j]) {
dist[i][j] = dist[i][k] + dist[k][j];
}
}
}
}
// 输出结果
printf("最短路径矩阵:\n");
for (i = 0; i < n; i++) {
for (j = 0; j < n; j++) {
if (dist[i][j] == INF) {
printf("INF ");
} else {
printf("%d ", dist[i][j]);
}
}
printf("\n");
}
}
int main() {
int graph[4][4] = {{0, 5, INF, 10},
{INF, 0, 3, INF},
{INF, INF, 0, 1},
{INF, INF, INF, 0}};
floyd(graph, 4);
return 0;
}
关键路径
概念,懂得手算
查找
查找是指在数据集合中寻找特定元素的过程。在计算机科学中,查找通常是指在数据结构中查找一个或多个特定值的过程
线性表的查找
顺序查找
int sequential_search(int arr[], int n, int x) {
int i;
for (i = 0; i < n; i++) { // 遍历数组
if (arr[i] == x) { // 如果找到了目标元素
return i; // 返回目标元素的下标
}
}
return -1; // 如果遍历完整个数组都没有找到目标元素,则返回-1
}
时间复杂度为 O ( n ) O(n) O(n)
折半查找
int binarySearch(int arr[], int x) {
int left = 1, right = arr.length();
while (left <= right) {
int mid = left + (right - left) / 2;
if (arr[mid] == x)
return mid;
if (arr[mid] < x)
left = mid + 1;
else
right = mid - 1;
}
return -1;
}
时间复杂度为 O ( l o g n ) O(logn) O(logn)
树表的查找
二叉排序树
二叉排序树(Binary Search Tree,简称BST)是一种特殊的二叉树,它满足以下性质:
- 若左子树不为空,则左子树上所有节点的值均小于它的根节点的值;
- 若右子树不为空,则右子树上所有节点的值均大于它的根节点的值;
- 左、右子树本身也分别为二叉排序树;
- 没有键值相等的节点。
简单来说,二叉排序树是一种能够快速查找、插入和删除数据的数据结构,它通过对节点的值进行比较,将数据存储在一个有序的二叉树中。
二叉排序树(Binary Search Tree)是一种特殊的二叉树,它的左子树上所有节点的值都小于根节点的值,右子树上所有节点的值都大于根节点的值。以下是二叉排序树的代码实现:
- 构造二叉排序树
typedef struct BSTNode {
int data;
struct BSTNode *left;
struct BSTNode *right;
} BSTNode, *BSTree;
// 初始化二叉排序树
void initBSTree(BSTree *tree) {
*tree = NULL;
}
// 创建新节点
BSTNode* createBSTNode(int data) {
BSTNode *node = (BSTNode*)malloc(sizeof(BSTNode));
node->data = data;
node->left = NULL;
node->right = NULL;
return node;
}
// 插入节点
void insertBSTNode(BSTree *tree, int data) {
if (*tree == NULL) {
*tree = createBSTNode(data);
return;
}
if (data < (*tree)->data) {
insertBSTNode(&((*tree)->left), data);
} else if (data > (*tree)->data) {
insertBSTNode(&((*tree)->right), data);
}
}
// 删除节点
二叉排序树的删除分为以下几个步骤:
1. 首先找到要删除的节点,如果该节点不存在,则删除失败。
2. 如果要删除的节点没有子节点,直接删除该节点即可。
3. 如果要删除的节点只有一个子节点,将该节点的子节点替换该节点即可。
4. 如果要删除的节点有两个子节点,需要找到该节点右子树中最小的节点,将该节点的值替换要删除的节点的值,然后再删除该最小节点。
void deleteBSTNode(BSTree *tree, int data) {
if (*tree == NULL) {
return;
}
if (data < (*tree)->data) {
deleteBSTNode(&((*tree)->left), data);
} else if (data > (*tree)->data) {
deleteBSTNode(&((*tree)->right), data);
} else {
if ((*tree)->left == NULL && (*tree)->right == NULL) {
free(*tree);
*tree = NULL;
} else if ((*tree)->left == NULL) {
BSTNode *temp = *tree;
*tree = (*tree)->right;
free(temp);
} else if ((*tree)->right == NULL) {
BSTNode *temp = *tree;
*tree = (*tree)->left;
free(temp);
} else {
BSTNode *temp = (*tree)->right;
while (temp->left != NULL) {
temp = temp->left;
}
(*tree)->data = temp->data;
deleteBSTNode(&((*tree)->right), temp->data);
}
}
}
// 查找节点
BSTNode* searchBSTNode(BSTree tree, int data) {
if (tree == NULL) {
return NULL;
}
if (data < tree->data) {
return searchBSTNode(tree->left, data);
} else if (data > tree->data) {
return searchBSTNode(tree->right, data);
} else {
return tree;
}
}
平衡二叉树
平衡二叉树是一种特殊的二叉搜索树,它的左右子树的高度差不超过1。这样可以保证在最坏情况下,树的高度为log(n),从而保证了树的查找、插入、删除等操作的时间复杂度为O(log(n))
左旋右旋自己看一下
B树
B树是一种自平衡的树形数据结构,它能够保持数据有序,并且能够进行快速的查找、插入和删除操作。B树通常应用于文件系统和数据库中,因为它能够有效地处理大量的数据。
B树的定义如下:
- 每个节点最多有m个子节点。
- 除了根节点和叶子节点外,每个节点至少有ceil(m/2)个子节点。
- 如果根节点不是叶子节点,则至少有两个子节点。
- 所有叶子节点都在同一层。
- 每个节点包含k-1个关键字,其中k是树的阶数。
- 关键字按照升序排列,对于任意一个非叶子节点,它的k-1个关键字将这个节点分成k个区间,每个区间对应一个子节点,且该子节点的所有关键字都大于前一个区间的关键字,小于后一个区间的关键字。
B树的阶数m通常是一个比较大的数,比如100或者1000,这样可以减少磁盘I/O操作的次数,提高查询效率。
B树是一种自平衡的树形数据结构,它能够保持数据有序,并且能够在对数时间内进行插入、删除和查找操作。B树通常应用于文件系统和数据库中,因为它能够高效地处理大量数据。
以下是B树的代码实现,包括构造、插入、删除和查找操作:
#include <stdio.h>
#include <stdlib.h>
#define M 3
typedef struct node {
int n; // 节点中关键字的个数
int keys[M - 1]; // 关键字数组
struct node *children[M]; // 子节点指针数组
int leaf; // 是否为叶子节点
} node;
node *root = NULL;
// 创建一个新节点
node *new_node(int key, node *child) {
node *new_node = (node *)malloc(sizeof(node));
new_node->n = 1;
new_node->keys[0] = key;
new_node->children[0] = root;
new_node->children[1] = child;
new_node->leaf = 0;
return new_node;
}
// 分裂节点
void split_child(node *parent, int i, node *child) {
node *new_child = (node *)malloc(sizeof(node));
new_child->n = M - 1;
new_child->leaf = child->leaf;
for (int j = 0; j < M - 1; j++) {
new_child->keys[j] = child->keys[j + M];
}
if (!child->leaf) {
for (int j = 0; j < M; j++) {
new_child->children[j] = child->children[j + M];
}
}
child->n = M - 1;
for (int j = parent->n; j >= i + 1; j--) {
parent->children[j + 1] = parent->children[j];
}
parent->children[i + 1] = new_child;
for (int j = parent->n - 1; j >= i; j--) {
parent->keys[j + 1] = parent->keys[j];
}
parent->keys[i] = child->keys[M - 1];
parent->n++;
}
// 插入关键字
void insert(int key) {
if (root == NULL) {
root = new_node(key, NULL);
return;
}
node *cur = root;
node *parent = NULL;
int i;
while (1) {
i = cur->n - 1;
while (i >= 0 && key < cur->keys[i]) {
cur->keys[i + 1] = cur->keys[i];
i--;
}
i++;
if (cur->leaf) {
break;
} else {
parent = cur;
cur = cur->children[i];
}
}
cur->keys[i] = key;
cur->n++;
if (cur->n == M) {
if (parent == NULL) {
parent = new_node(0, NULL);
root = parent;
parent->children[0] = cur;
split_child(parent, 0, cur);
} else {
split_child(parent, i, cur);
}
}
}
// 删除关键字
void delete(int key) {
if (root == NULL) {
return;
}
node *cur = root;
node *parent = NULL;
int i;
while (1) {
i = cur->n - 1;
while (i >= 0 && key < cur->keys[i]) {
i--;
}
if (cur->keys[i] == key) {
break;
}
if (cur->leaf) {
return;
}
parent = cur;
cur = cur->children[i];
}
if (cur->leaf) {
for (int j = i; j < cur->n - 1; j++) {
cur->keys[j] = cur->keys[j + 1];
}
cur->n--;
} else {
node *left_child = cur->children[i];
node *right_child = cur->children[i + 1];
if (left_child->n >= M) {
int pred = left_child->keys[left_child->n - 1];
delete(pred);
cur->keys[i] = pred;
return;
} else if (right_child->n >= M) {
int succ = right_child->keys[0];
delete(succ);
cur->keys[i] = succ;
return;
} else {
left_child->keys[left_child->n] = key;
for (int j = 0; j < M - 1; j++) {
left_child->keys[left_child->n + 1 + j] = right_child->keys[j];
}
for (int j = 0; j < M; j++) {
left_child->children[left_child->n + 1 + j] = right_child->children[j];
}
left_child->n += right_child->n + 1;
for (int j = i; j < cur->n - 1; j++) {
cur->keys[j] = cur->keys[j + 1];
}
for (int j = i + 1; j < cur->n; j++) {
cur->children[j] = cur->children[j + 1];
}
cur->n--;
free(right_child);
if (cur->n == 0) {
if (parent == NULL) {
root = left_child;
} else {
delete(cur->keys[0]);
}
free(cur);
}
}
}
}
// 查找关键字
int search(int key) {
node *cur = root;
int i;
while (cur != NULL) {
i = cur->n - 1;
while (i >= 0 && key < cur->keys[i]) {
i--;
}
if (cur->keys[i] == key) {
return 1;
}
if (cur->leaf) {
return 0;
}
cur = cur->children[i];
}
return 0;
}
int main() {
insert(1);
insert(2);
insert(3);
insert(4);
insert(5);
insert(6);
insert(7);
insert(8);
insert(9);
delete(5);
printf("%d\n", search(5));
printf("%d\n", search(6));
return 0;
}
B+树
B+树是一种多路搜索树,它的定义和B树类似,但是B+树有一些特殊的性质。B+树的定义如下:
- 每个节点最多有M个子节点。
- 除根节点和叶子节点外,每个节点至少有M/2个子节点。
- 所有叶子节点都在同一层,且不存储数据,只存储指向数据的指针。
- 所有非叶子节点只存储索引信息,不存储数据。
B+树和B树的区别在于:
- B+树的所有数据都存储在叶子节点中,而B树的数据可以存储在任何节点中。
- B+树的非叶子节点只存储索引信息,而B树的非叶子节点既存储索引信息,也存储数据。
- B+树的叶子节点之间有指针相连,可以方便地进行范围查询和遍历操作。
因为B+树的特殊性质,它在数据库索引、文件系统等领域得到了广泛应用。
散列表的查找
了解一下过程
排序
插入排序
直接插入排序
直接插入排序是一种简单的排序算法,其基本思想是将待排序的元素插入到已经排好序的序列中,从而得到一个新的、更大的有序序列。以下是直接插入排序的C语言代码实现:
void insertionSort(int arr[], int n) {
int i, j, key;
for (i = 1; i < n; i++) {
key = arr[i];
j = i - 1;
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
j = j - 1;
}
arr[j + 1] = key;
}
}
时间复杂度: O ( n 2 ) O(n^2) O(n2)
空间复杂度: O ( 1 ) O(1) O(1)
直接插入排序的时间复杂度为 O ( n 2 ) O(n^2) O(n2),空间复杂度为 O ( 1 ) O(1) O(1)。
- 稳定性:直接插入排序是一种稳定的排序算法,即相同元素的相对位置在排序前后不会发生改变。
- 特点:直接插入排序适用于数据量较小的排序,实现简单,但是对于数据量较大的排序,效率较低。
折半插入排序
折半插入排序是一种插入排序算法,它的基本思想是将待排序的序列分为两部分,前一部分是已经排好序的,后一部分是待排序的。每次从后一部分中取出一个元素,将它插入到前一部分中的适当位置,使得插入后前一部分仍然有序。下面是折半插入排序的算法描述和C语言代码实现:
算法描述:
- 将待排序序列分为已排序和未排序两部分,初始时已排序部分只有一个元素。
- 从未排序部分取出一个元素,将它与已排序部分的元素依次比较,找到插入位置。
- 通过二分查找法找到插入位置,将已排序部分中大于该元素的元素向后移动一个位置。
- 将该元素插入到已排序部分的正确位置。
- 重复步骤2-4,直到未排序部分中的元素全部插入到已排序部分中。
C语言代码实现:
void binaryInsertSort(int arr[], int n) {
int i, j, left, right, mid, temp;
for (i = 1; i < n; i++) {
temp = arr[i];
left = 0;
right = i - 1;
while (left <= right) {
mid = (left + right) / 2;
if (arr[mid] > temp) {
right = mid - 1;
} else {
left = mid + 1;
}
}
for (j = i - 1; j >= left; j--) {
arr[j + 1] = arr[j];
}
arr[left] = temp;
}
}
时间复杂度为
O
(
n
2
)
O(n^2)
O(n2),空间复杂度为
O
(
1
)
O(1)
O(1)
特点:
- 稳定排序
- 只能用于顺序结构,不能用于链式结构
- 适合初试记录无序,n较大的情况
希尔排序
希尔排序是一种高效的排序算法,其基本思想是将待排序的元素按照一定的间隔分组,对每组使用插入排序算法进行排序,然后逐步缩小间隔直至为1,最后使用插入排序算法对整个序列进行排序。
void shellSort(int arr[], int n) {
int gap, i, j, temp;
for (gap = n / 2; gap > 0; gap /= 2) {
for (i = gap; i < n; i++) {
temp = arr[i];
for (j = i; j >= gap && arr[j - gap] > temp; j -= gap) {
arr[j] = arr[j - gap];
}
arr[j] = temp;
}
}
}
其中,arr为待排序的数组,n为数组的长度。在代码中,我们首先定义了一个gap变量,表示间隔的大小,初始值为n/2。然后,我们使用一个循环来不断缩小gap的值,直至为1。在每次循环中,我们使用插入排序算法对每个子序列进行排序,直到整个序列有序。
手动,每一趟,需要自己能写
希尔排序比插入排序和冒泡排序更快,但比快速排序、归并排序和堆排序慢。
空间复杂度为
O
(
1
)
O(1)
O(1)
特点:
- 记录跳跃式地移动导致排序方法是不稳定的
- 只能用于顺序,不能链式
- 适合初试记录无序,且n较大
交换排序
冒泡排序
冒泡排序是一种简单的排序算法,其基本思想是通过不断比较相邻的元素,将较大的元素逐步交换到右侧,从而实现排序的目的。以下是冒泡排序的算法描述和C语言代码实现:
算法描述:
- 比较相邻的元素。如果第一个比第二个大,就交换它们两个;
- 对每一对相邻元素做同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数;
- 针对所有的元素重复以上的步骤,除了最后一个;
- 持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。
C语言代码实现:
void bubbleSort(int arr[], int n) {
int i, j, temp;
for (i = 0; i < n - 1; i++) {
for (j = 0; j < n - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
手动
移动记录次数较多,算法平均时间性能比直接插入排序差
快速排序
快速排序是一种高效的排序算法,其算法描述如下:
- 从数列中挑出一个元素,称为 “基准”(pivot);
- 重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆放在基准的后面(相同的数可以到任一边)。在这个分区结束之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
- 递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
#include <stdio.h>
void swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
int partition(int arr[], int low, int high) {
int pivot = arr[high];
int i = (low - 1);
for (int j = low; j <= high - 1; j++) {
if (arr[j] < pivot) {
i++;
swap(&arr[i], &arr[j]);
}
}
swap(&arr[i + 1], &arr[high]);
return (i + 1);}
void quickSort(int arr[], int low, int high) {
if (low < high) {
int pi = partition(arr, low, high);
quickSort(arr, low, pi - 1);
quickSort(arr, pi + 1, high);
}
}
int main() {
int arr[] = {10, 7, 8, 9, 1, 5};
int n = sizeof(arr) / sizeof(arr[0]);
quickSort(arr, 0, n - 1);
printf("Sorted array: ");
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
return 0;
}
手动
时间复杂度:
- 最优情况下,每次划分都能将数组均匀地分成两部分,时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)
- 最差情况下,每次划分只能将数组分成一部分和剩余的另一部分,时间复杂度为 O ( n 2 ) O(n^2) O(n2)。
- 平均情况下,时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn)。
空间复杂度:
- 最优情况下,每次划分都能将数组均匀地分成两部分,递归调用的栈深度为 O ( l o g n ) O(logn) O(logn),空间复杂度为 O ( l o g n ) O(logn) O(logn)。
- 最差情况下,每次划分只能将数组分成一部分和剩余的另一部分,递归调用的栈深度为 O ( n ) O(n) O(n),空间复杂度为 O ( n ) O(n) O(n)。
- 平均情况下,空间复杂度为 O ( l o g n ) O(logn) O(logn)。
选择排序
简单选择排序
以下是简单选择排序的算法描述和代码,使用C语言实现:
算法描述:
- 遍历数组,找到最小的元素。
- 将最小元素与数组的第一个元素交换位置。
- 从第二个元素开始,重复步骤1和2,直到整个数组有序。
代码实现:
void SelectSort(int* arr, int size) {
int i, j, minIndex, temp;
for (i = 0; i < size - 1; i++) {
minIndex = i;
for (j = i + 1; j < size; j++) {
if (arr[j] < arr[minIndex]) {
minIndex = j;
}
}
if (minIndex != i) {
temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
}
}
堆排序
堆排序是一种树形选择排序,它的时间复杂度为O(nlogn),是对直接选择排序的有效改进。下面是堆排序的算法描述和C语言代码实现:
算法描述:
- 将待排序序列构造成一个大顶堆或小顶堆。
- 此时,整个序列的最大值或最小值就是堆顶的根节点。
- 将其与末尾元素进行交换,此时末尾就为最大值或最小值。
- 然后将剩余n-1个元素重新构造成一个堆,这样会得到n个元素的次小值。
- 如此反复执行,便能得到一个有序序列。
C语言代码实现:
#include <stdio.h>
// 交换两个元素的值
void swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}
// 堆调整函数
void heapAdjust(int arr[], int i, int n) {
int child, temp;
for (temp = arr[i]; 2 * i + 1 < n; i = child) {
child = 2 * i + 1;
if (child < n - 1 && arr[child + 1] > arr[child]) {
child++;
}
if (temp < arr[child]) {
arr[i] = arr[child];
} else {
break;
}
}
arr[i] = temp;
}
// 堆排序函数
void heapSort(int arr[], int n) {
int i;
// 构建堆
for (i = n / 2 - 1; i >= 0; i--) {
heapAdjust(arr, i, n);
}
// 排序
for (i = n - 1; i > 0; i--) {
swap(&arr[0], &arr[i]);
heapAdjust(arr, 0, i);
}
}
int main() {
int arr[] = { 12, 11, 13, 5, 6, 7 };
int n = sizeof(arr) / sizeof(arr[0]);
heapSort(arr, n);
printf("排序后的数组:\n");
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
return 0;
}
手动
快速排序是一种高效的排序算法,其时间复杂度为 O ( n l o g n ) O(nlogn) O(nlogn),空间复杂度为 O ( l o g n ) O(logn) O(logn)。
归并排序
归并排序是一种基于分治思想的排序算法,其主要思想是将待排序数组分成两个子数组,对这两个子数组分别进行排序,最后将排好序的子数组合并成一个有序的数组。其算法描述如下:
- 分解:将待排序数组从中间分成两个子数组,直到每个子数组只有一个元素为止。
- 合并:将两个有序的子数组合并成一个有序的数组。
#include <stdio.h>
#include <stdlib.h>
void merge(int arr[], int left, int mid, int right) {
int i, j, k;
int n1 = mid - left + 1;
int n2 = right - mid;
int L[n1], R[n2];
for (i = 0; i < n1; i++) {
L[i] = arr[left + i];
}
for (j = 0; j < n2; j++) {
R[j] = arr[mid + 1 + j];
}
i = 0;
j = 0;
k = left;
while (i < n1 && j < n2) {
if (L[i] <= R[j]) {
arr[k] = L[i];
i++;
} else {
arr[k] = R[j];
j++;
}
k++;
}
while (i < n1) {
arr[k] = L[i];
i++;
k++; }
while (j < n2) {
arr[k] = R[j];
j++;
k++;
}
}
void mergeSort(int arr[], int left, int right) {
if (left < right) {
int mid = left + (right - left) / 2;
mergeSort(arr, left, mid);
mergeSort(arr, mid + 1, right);
merge(arr, left, mid, right);
}
}
int main() {
int arr[] = {38, 27, 43, 3, 9, 82, 10};
int n = sizeof(arr) / sizeof(arr[0]);
int i;
printf("Original array: ");
for (i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
mergeSort(arr, 0, n - 1);
printf("\nSorted array: ");
for (i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
return 0;
}
归并排序是一种基于分治思想的排序算法,其时间复杂度为
O
(
n
l
o
g
n
)
O(nlogn)
O(nlogn),空间复杂度为
O
(
n
)
O(n)
O(n)。
动态规划
动态规划是一种解决多阶段决策过程最优化问题的数学思想。它将原问题分解为若干个子问题,通过求解子问题的最优解,来推导出原问题的最优解。动态规划的核心思想是“最优化原理”和“无后效性原则”。其中,“最优化原理”指的是在每一阶段都选择当前状态下的最优决策,从而导致全局最优解;“无后效性原则”指的是某个状态一旦确定,就不受之后决策的影响。动态规划的设计方法有正推和倒推两种方式,可以看作是记忆化搜索的一种优化方法。
背包
01背包
动态规划01背包问题的描述和代码如下所示:
问题描述:
给定 n 件物品,物品的重量为 w[i],物品的价值为 c[i]。现挑选物品放入背包中,假定背包能承受的最大重量为 V,问应该如何选择装入背包中的物品,使得装入背包中物品的总价值最大?
解决方案:
动态规划是解决01背包问题的一种常用方法。我们可以使用一个二维数组 d p [ i ] [ j ] dp[i][j] dp[i][j]表示前 i i i个物品放入容量为j的背包中所能获得的最大价值。则状态转移方程为:
d
p
[
i
]
[
j
]
=
m
a
x
(
d
p
[
i
−
1
]
[
j
]
,
d
p
[
i
−
1
]
[
j
−
w
[
i
]
]
+
c
[
i
]
)
dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i]] + c[i])
dp[i][j]=max(dp[i−1][j],dp[i−1][j−w[i]]+c[i])
其中,
d
p
[
i
−
1
]
[
j
]
dp[i-1][j]
dp[i−1][j]表示不放第i个物品时的最大价值,
d
p
[
i
−
1
]
[
j
−
w
[
i
]
]
+
c
[
i
]
dp[i-1][j-w[i]] + c[i]
dp[i−1][j−w[i]]+c[i]表示放第i个物品时的最大价值。
代码实现:
#include <stdio.h>
#define MAX_N 1000
#define MAX_V 1000
int w[MAX_N], c[MAX_N];
int dp[MAX_N][MAX_V];
int max(int a, int b) {
return a > b ? a : b;
}
int main() {
int n, V;
scanf("%d%d", &n, &V);
for (int i = 1; i <= n; i++) {
scanf("%d%d", &w[i], &c[i]);
}
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= V; j++) {
if (j < w[i]) {
dp[i][j] = dp[i-1][j];
} else {
dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i]] + c[i]);
}
}
}
printf("%d\n", dp[n][V]);
return 0;
}
多重背包
动态规划多重背包问题是指每个物品有一定的数量限制,可以选择多次放入背包中,求解在背包容量有限的情况下,能够获得的最大价值。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define max(a, b) ((a) > (b) ? (a) : (b))
int main()
{
int n, m;
scanf("%d%d", &n, &m);
int w[n + 1], v[n + 1], s[n + 1];
for (int i = 1; i <= n; i++) {
scanf("%d%d%d", &w[i], &v[i], &s[i]);
}
int dp[m + 1];
memset(dp, 0, sizeof(dp));
for (int i = 1; i <= n; i++) {
for (int j = m; j >= 0; j--) {
for (int k = 0; k <= s[i] && k * w[i] <= j; k++) {
dp[j] = max(dp[j], dp[j - k * w[i]] + k * v[i]);
}
}
}
printf("%d\n", dp[m]);
return 0;
}
其中,n表示物品的数量,m表示背包的容量,w[i]表示第i个物品的重量,v[i]表示第i个物品的价值,s[i]表示第i个物品的数量限制。dp[j]表示背包容量为j时能够获得的最大价值。