前置知识:c语言知识准备
(1)结构体struct
指针与结构体
struct point{
int x;
int y;
};
struct point p;
struct point *pp;
pp = &p; // 可以通过指针给该结构体内的变量赋值
// 方式一:
(*pp).x = 5;
// 方式二:
pp -> y = 10;
(2)typedef
typedef 数据类型 别名
(3)动态内存分配
指针 = (数据类型*)malloc(字节数);
(4)字符串初始化
c语言中没有内置string类型,以下列方式初始化
include <string.h>
int main(){
...
// str1 = str2
stripy(str1,str2)
序言
1.算法分析
算法复杂度 = 时间复杂度(T(n) = O(f(n))) + 空间复杂度 取n为无穷
时间频度 = f(n)
O(1) < O(logn) < O(n) < O(nlogn) < O(n^2) < O(n^3) < O(2^n) < O(n!) < O(n^n)
计算时间复杂度(抓大头):
int sum = 0;
for(int i = 1;i<n;i*=2){ // 一层循环
for(int j = 0;j<i;j++){ // 二层循环
sum++;
}
}
| 次数 | 1 | 2 | ... | t |
| i | 2^0 | 2^1 | ... | 2^(t-1)=n |
| 内层次数 | 1 | 1+2 | ... | 1+2+4+...+2^(t-1) |
f(n) = 1 + 2 + ... + 2^(t-1) = (1-2^t)/(1-2) = 2^t = O(n)
2.ADT(=Abstract Data Type) 抽象数据接口
对内隐藏细节,对外暴露接口。
组成(数据对象,数据关系和基本操作):
数据表示:定义数据的存储结构。
操作接口:定义可以在数据上进行的操作。
一、线性表
概念:由零个或多个具有相同类型的结点组成的有序集合。
线性表中的元素之间存在一对一的关系,也就是说每个元素都有一个直接前驱和一个直接后继,除了第一个元素没有前驱,最后一个元素没有后继。线性表可以用来表示各种具有线性关系的数据,例如数组(顺序表)、链表等。
1.顺序表
用一组连续的存储单元依次存储线性表的各个元素,也就是说,逻辑上相邻的元素,实际上的物理存储空间也是连续的。

顺序表的操作
a.顺序表存储结构
# define MAXSIZE = 100
typedef int ElemType; // 增强代码的可复用性
typedef struct{
ElemType data[MAXSIZE];
int length;
}SeqList;
b.初始化
void initList(SeqList* L)
{
int i = 0;
for (i = 0;i < MAXSIZE;i++)
{
L->data[i] = 0;
}
L->length = 0;
}
c.在尾部添加元素
int appendElem(SeqList *L, ElemType e){
if (L->length >= MAXSIZE){
printf("顺序表已满\n");
return 0;
}
L->data[L->length] = e;
L->length++;
return 1;
}
d.遍历
void listElem(SeqList *L){
for(int i=0;i < L->length;i++){
printf("%d ", L->data[i]);
}
printf("\n");
}
e.插入元素
// pos <= length
int insertElem(SeqList *L, int pos, ElemType e){
for(int i = L->length; i >= pos ; i--){
L->data[i] = L->data[i-1];
}
L->data[pos-1] = e;
L->length++;
return 1;
}
f.删除元素
本质是用后一个元素覆盖掉前面的。
int deleteElem(SeqList *L, int pos){
for(int i = pos-1; i < L->length ; i++){
L->data[i] = L->data[i+1];
}
L->length--;
return 1;
}
g.查找
int findElem(SeqList *L, ElemType e){
for(int i = 0; i < L->length; i++){
if (L->data[i] == e){
return i + 1;
}
}
return 0;
}
h.动态分配内存地址初始化
仍可调用上面的函数,只是初始化方式发生变化。
# define MAXSIZE = 100
typedef int ElemType; // 增强代码的可复用性
typedef struct{
ElemType *data;
int length;
}SeqList;
SeqList* initList(){
SeqList* L = (SeqList*)malloc(sizeof(SeqList));
L->data = (ElemType*)malloc(sizeof(Elemtype) * MAXSIZE);
L->length = 0;
return L;
}
i.完整代码
#include<stdio.h>
#define MAXSIZE 100
typedef int ElemType;
typedef struct {
int data[MAXSIZE];
int length;
}SeqList;
// 初始化顺序表
void initList(SeqList* L)
{
int i = 0;
for (i = 0; i < MAXSIZE; i++)
{
L->data[i] = 0;
}
L->length = 0;
}
// 在尾部插入元素
int appendElem(SeqList* L, ElemType e) {
if (L->length >= MAXSIZE) {
printf("顺序表已满\n");
return 0;
}
L->data[L->length] = e;
L->length++;
return 1;
}
// 遍历顺序表
void listElem(SeqList* L) {
for (int i = 0; i < L->length; i++) {
printf("%d ", L->data[i]);
}
printf("\n");
}
// 在pos处插入元素e(pos <= length)
int insertElem(SeqList* L, int pos, ElemType e) {
for (int i = L->length; i >= pos; i--) {
L->data[i] = L->data[i - 1];
}
L->data[pos - 1] = e;
L->length++;
return 1;
}
// 删除元素
int deleteElem(SeqList* L, int pos) {
for (int i = pos - 1; i < L->length; i++) {
L->data[i] = L->data[i + 1];
}
L->length--;
return 1;
}
// 查找元素
int findElem(SeqList* L, ElemType e) {
for (int i = 0; i < L->length; i++) {
if (L->data[i] == e) {
return i + 1;
}
}
return 0;
}
int main() {
SeqList L;
// 初始化顺序表
initList(&L);
// 将顺序表变为[5, 4, 2]
appendElem(&L, 5);
appendElem(&L, 4);
appendElem(&L, 2);
// 打印操作后的顺序表
listElem(&L);
// 将顺序表变为[5, 8, 3, 4, 2]
insertElem(&L, 2, 8);
insertElem(&L, 3, 3);
// 打印操作后的顺序表
listElem(&L);
// 将顺序表变为[5, 3, 4, 2]
deleteElem(&L, 2);
// 打印操作后的顺序表
listElem(&L);
// 找到e=3的位置
printf("%d",findElem(&L, 3));
}
2.链表
线性表链式存储的特点是:用一组任意的存储单元存储线性表的数据元素(这组存储单元可以是连续的,也可以是不连续的)。
为了表示每个数据元素与其直接后继元素之间的逻辑关系,每个数据元素除了其本身的信息之外,还需要存储一个指示其直接后继的信息(直接后继的存储位置)。这两部分构成一个数据元素的存储映像,成为结点(node)。
结点包括两个域:其中存储数据元素信息的称为数据域,存储直接后继存储位置的称为指针域。指针域中存储的信息称为指针或链。

2.1单链表
typedef struct Node{
ElemType data; // 数据域
struct Node* next; // 指针域
}Node;
a.初始化
Node* initNode(){
Node* head = (Node*)malloc(sizeof(Node)); // 创建头指针
head->data = 0;
head->next = NULL;
return head;
}
b.头插法
首先创建一个新节点,并为其分配内存,然后将新节点的 next 指针指向头结点的下一个结点,最后将头结点的 next 指针指向新结点。
依次插入 10,20,30 --> 30,20,10
int insertHead(Node* N, ElemType e){
Node* p = (Node*)malloc(sizeof(Node));
p->data = e;
p->next = N->next;
N->next = p;
return 1;
}
c.尾插法
依次插入 10,20,30 --> 10,20,30
// 获取尾结点
Node* get_tail(Node* N){
Node* p = N;
while(p->next != NULL){
p = p->next;
}
return p;
}
// 尾插法
int insertTail(Node* tail, ElemType e){
Node* p = (Node*)malloc(sizeof(Node));
tail->next = p;
p->data = e;
p->next = NULL;
return 1;
}
d.指定位置插入元素
int insertNode(Node* N, int pos,ElemType e){
Node* p = N; // p指向头结点(非第一个结点)
int i = 0;
while(i < pos-1){
p = p->next;
i++;
if (p == NULL){
return 0;
}
}
Node* q = (Node*)malloc(sizeof(Node));
q->data = e;
q->next = p->next;
p->next = q;
return 1;
}
e.删除结点
int deleteNode(Node* N, int pos){
Node* p = N;
int i = 0;
while(i < pos-1){
p = p->next;
i++;
if (p == NULL){
return 0;
}
}
if (p->next = NULL){
return 0;
}
Node *q = p->next;
p->next = q->next;
free(q);
return 1;
}
f.查找元素
// 查找指定位置的元素
int findposNode(Node* N, int pos){
Node* p = N; // p指向头结点(非第一个结点)
int i = 0;
while(i < pos){
p = p->next;
i++;
if (p == NULL){
return 0;
}
}
return p->data;
}
// 查找第一个值为key的结点
int findkeyNode(Node* N, ElemType key){
Node* p = N->next; // p指向第一个结点
int i = 0;
while(p != NULL){
if (p->data == key){
return i+1;
}
p = p->next;
i++;
}
return 0;
}
g.释放链表
void freeNode(Node* N){
Node* p = N->next;
Node* q = p;
while(p != NULL){
q = p->next;
free(p);
p = q;
}
N->next = NULL;
}
h.完整代码
#include<stdio.h>
#include<stdlib.h>
typedef int ElemType;
typedef struct Node {
ElemType data; // 数据域
struct Node* next; // 指针域
}Node;
// 初始化
Node* initNode() {
Node* head = (Node*)malloc(sizeof(Node)); // 创建头指针
head->data = 0;
head->next = NULL;
return head;
}
// 头插法
int insertHead(Node* N, ElemType e) {
Node* p = (Node*)malloc(sizeof(Node));
p->next = N->next;
N->next = p;
p->data = e;
return 1;
}
// 获取尾结点
Node* get_tail(Node* N) {
Node* p = N;
while (p->next != NULL) {
p = p->next;
}
return p;
}
// 尾插法
int insertTail(Node* tail, ElemType e) {
Node* p = (Node*)malloc(sizeof(Node));
tail->next = p;
p->data = e;
p->next = NULL;
return 1;
}
// 在指定位置插入元素
int insertNode(Node* N, int pos, ElemType e) {
Node* p = N; // p指向头结点(非第一个结点)
int i = 0;
while (i < pos - 1) {
p = p->next;
i++;
if (p == NULL) {
return 0;
}
}
Node* q = (Node*)malloc(sizeof(Node));
q->data = e;
q->next = p->next;
p->next = q;
return 1;
}
// 删除结点
int deleteNode(Node* N, int pos) {
Node* p = N;
int i = 0;
while (i < pos - 1) {
p = p->next;
i++;
if (p == NULL) {
return 0;
}
}
if (p->next = NULL) {
return 0;
}
Node* q = p->next;
p->next = q->next;
free(q);
return 1;
}
// 查找指定位置的元素
int findposNode(Node* N, int pos) {
Node* p = N; // p指向头结点(非第一个结点)
int i = 0;
while (i < pos) {
p = p->next;
i++;
if (p == NULL) {
return 0;
}
}
return p->data;
}
// 查找第一个值为key的结点
int findkeyNode(Node* N, ElemType key) {
Node* p = N->next; // p指向第一个结点
int i = 0;
while (p != NULL) {
if (p->data == key) {
return i + 1;
}
p = p->next;
i++;
}
return 0;
}
// 打印链表
void printNode(Node* N) {
Node* p = N->next;
while (p != NULL) {
printf("%d ", p->data);
p = p->next;
}
printf("\n");
}
// 释放链表
void freeNode(Node* N) {
Node* p = N->next;
Node* q = p;
while (p != NULL) {
q = p->next;
free(p);
p = q;
}
N->next = NULL;
}
int main() {
// 头插法:依次插入10,20,30
Node* N1 = initNode();
insertHead(N1, 10);
insertHead(N1, 20);
insertHead(N1, 30);
printf("头插法结果:");
printNode(N1);
insertNode(N1, 2, 15);
insertNode(N1, 4, 25);
printNode(N1);
printf("N1第一个值为10的是第%d个结点\n", findkeyNode(N1, 10));
// 尾插法:依次插入10,20,30
Node* N2 = initNode();
insertTail(get_tail(N2), 10);
insertTail(get_tail(N2), 20);
insertTail(get_tail(N2), 30);
printf("尾插法结果:");
printNode(N2);
printf("N2第二个结点的值为%d\n", findposNode(N2, 2));
freeNode(N1);
freeNode(N2);
return 0;
}

2.2双向链表

a.双向链表存储结构
typedef struct{
DoublyNode* prev; //指向直接前趋
ElemType data;
DoublyNode* next; //指向直接后继
}DoublyNode;
b.删除结点
只是为了展示删除时候指针的逻辑关系。
int deleteDoublyNode(DoublyNode* N, int pos){
DoublyNode* p = N;
int i = 0;
while(i < pos){
p = p->next;
i++;
if (p == NULL){
return 0;
}
}
p->prev->next = p->next;
// 假设p的next不为NULL
p->next->prev = p->prev;
free(p);
return 1;
}
2.3单向循环链表
尾指针的next指向头指针。
2.4链表应用
2.4.1 双指针--快慢指针
示例一:返回链表的倒数第k个结点。(给定的 k 保证是有效的)
先让快指针走k步,后快慢指针一起走,当快指针指向空值时,慢指针指向的值即为目标值。
int findNodeFS(Node* head, int k){
Node* fast = head;
Node* slow = head;
for(int i = 0;i<k;i++){
fast = fast->next;
}
while( fast->next != NULL){
fast = fast->next;
slow = slow->next;
}
return slow->data;
}
示例二:判断一个链表是否有环。
快指针走两步的同时慢指针走一步,如果他们两个能相遇,即有环。
int isCycle(Node* head){
Node* fast = head;
Node* slow = head;
while(fast != NULL && fast->next != NULL){
fast = fast->next->next;
slow = slow->next;
if (fast == slow){
return 1;
}
}
return 0;
}
2.4.2 空间置换时间
示例:在单链表中删除所有绝对值相等的结点,只保留首次出现的结点。
void removeNode(Node* head){
Node* p = head;
Node* pre = NULL; // 保存前置结点,便于删除q
ElemType flag[10000];
for(int i=0;i<10000;i++){
flag[i]=0;
}
while(p != NULL){
if(flag[abs(p->data)] == 0){
flag[abs(p->data)] = 1;
pre = p;
p = p->next;
}
else{
Node *q = p;
pre->next = p->next;
free(q);
p = p->next;
}
}
}
3.顺序表与链表
没有最适合所有操作的数据结构。


二、栈和队列
1.栈(Stack)- LIFO
概念:栈是限定仅在表尾进行插入或删除操作的顺序表。因此对栈来说,表尾端有其特殊含义,称为栈顶(top),相应的,表头称为栈底(bottom)。不含元素的空表称为空栈。
对栈的基本操作有进栈(push)和出栈(pop)。
1.1栈的顺序存储结构
#define MAXSIZE 100
typedef int ElemType;
typedef struct{
ElemType element[MAXSIZE];
int top; // 栈顶位置
}Stack;
1.1.1.初始化
void initStack(Stack *s){
s->top = -1;
}
1.1.2.判断栈是否为空
int isEmpty(Stack *s){
if (s->top == -1){
return 1;
}
return 0;
}
1.1.3.入栈/压栈
int push(Stack *s, ElemType e){
if (s->top >= MAXSIZE - 1){
printf("栈满了!\n");
return 0;
}
s->top++;
s->element[s->top] = e;
return 1;
}
1.1.4.出栈
int pop(Stack *s, ElemType *e){
if (isEmpty(s)){
return 0;
}
*e = s->element[s->top];
s->top--;
return 1;
}
1.1.5.获取栈顶元素
int getTop(Stack *s, ElemType *e){
if (isEmpty(s)){
return 0;
}
*e = s->element[s->top];
return 1;
}
1.1.6.完整代码实现
#include<stdio.h>
#define MAXSIZE 100
typedef int ElemType;
typedef struct {
ElemType element[MAXSIZE];
int top; // 栈顶位置
}Stack;
// 初始化
void initStack(Stack* s) {
s->top = -1;
}
// 判断栈是否为空
int isEmpty(Stack* s) {
if (s->top == -1) {
return 1;
}
return 0;
}
// 入栈/压栈
int push(Stack* s, ElemType e) {
if (s->top >= MAXSIZE - 1) {
printf("栈满了!\n");
return 0;
}
s->top++;
s->element[s->top] = e;
return 1;
}
// 出栈
int pop(Stack* s, ElemType* e) {
if (isEmpty(s)) {
return 0;
}
*e = s->element[s->top];
s->top--;
return 1;
}
// 获取栈顶元素
int getTop(Stack* s, ElemType* e) {
if (isEmpty(s)) {
return 0;
}
*e = s->element[s->top];
return 1;
}
int main() {
Stack s;
ElemType e;
initStack(&s);
printf("%d\n", isEmpty(&s));
push(&s, 4);
push(&s, 7);
push(&s, 9);
push(&s, 10);
// 此时栈顶元素为 10
getTop(&s, &e);
printf("此时栈顶元素为%d\n", e);
pop(&s, &e);
printf("此时出栈元素为%d\n", e);
pop(&s, &e);
printf("此时出栈元素为%d\n", e);
return 0;
}

1.2栈的链式存储结构
typedef int ElemType;
typedef struct{
ElemType element;
Stack *next;
}Stack;
1.2.1.初始化
void initStack(){
Stack *s = (Stack*)malloc(sizeof(Stack));
s->element = 0;
s->next = NULL;
return s;
}
1.2.2.判断栈是否为空
int isEmpty(Stack *s){
if (s->next == NULL){
return 1;
}
return 0;
}
1.2.3.入栈/压栈
利用头插法时间复杂度最小,来满足栈后进先出的规则。
void push(Stack *s, ElemType e){
Stack *p = (Stack*)malloc(sizeof(Stack));
p->element = e;
p->next = s->next;
s->next = p;
}
1.2.4.出栈
int pop(Stack *s, ElemType *e){
if (isEmpty(s)){
return 0;
}
*e = s->next->element;
Stack *q = s->next;
s->next = q->next;
free(q);
return 1;
}
1.2.5.获取栈顶元素
int getTop(Stack *s, ElemType *e){
if (isEmpty(s)){
return 0;
}
*e = s->next->element;
return 1;
}
1.3.栈的应用
1.3.1.表达式求值
1.3.2.迷宫求解
2.队列(Queue)- FIFO
概念:只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。队列中没有元素时,称为空队列。
队列的基本操作:入队,出队,判断队列是否为空和获取队头元素。

用单链表实现链式队列时,队头对应链头,队尾对应链尾。
为了节省内存,避免大量出队后队头空置造成的资源浪费,所以只研究循环队列。
循环队列重点:
① 判断队列为空:rear == front
② 判断队列为满:(rear + 1) % MAXSIZE == front
2.1.队列的顺序存储结构
#include <stdio.h>
#include <stdlib.h>
typedef int T;
typedef struct {
T* data;
int front;
int rear;
int MAXSIZE;
}SeqQueue;
SeqQueue* SQ_Create(int MAXSIZE)
{
SeqQueue* sq = (SeqQueue*)malloc(sizeof(SeqQueue));
sq->data = (T*)malloc(sizeof(T) * MAXSIZE);
sq->front = sq->rear = 0;
sq->MAXSIZE = MAXSIZE;
return sq;
}
void SQ_Free(SeqQueue* sq)
// 释放队列空间,以删除队列
{
free(sq->data);
free(sq);
}
void SQ_MakeEmpty(SeqQueue* sq)
// 将队列置空
{
sq->front = 0;
sq->rear = 0;
}
int SQ_IsEmpty(SeqQueue* sq)
// 判断队列是否为空,为空返回true,否则返回false
{
if (sq->front == sq->rear) {
return 1;
}
return 0;
}
int SQ_IsFull(SeqQueue* sq)
// 判断队列是否为满。为满返回true,否则返回false
{
if ((sq->rear + 1) % sq->MAXSIZE == sq->front) {
return 1;
}
return 0;
}
int SQ_Length(SeqQueue* sq)
// 队列长度
{
return (sq->rear - sq->front + sq->MAXSIZE) % sq->MAXSIZE;
}
int SQ_In(SeqQueue* sq, T x)
// 将x入队。若入队失败(队列满),则返回false,否则返回true
{
if (SQ_IsFull(sq)) {
return 0;
}
sq->data[sq->rear] = x;
sq->rear = (sq->rear + 1) % sq->MAXSIZE;
return 1;
}
int SQ_Out(SeqQueue* sq, T& item)
// 从队列sq出队一个元素,返回时item为出队的元素的值。若出队成功(队列不为空),则返回true,否则(队列空),返回false,此时item不会返回有效值
{
if (SQ_IsEmpty(sq)) {
return 0;
}
item = sq->data[sq->front];
sq->front = (sq->front + 1) % sq->MAXSIZE;
return 1;
}
int SQ_Head(SeqQueue* sq, T& head)
// 获取队列头结点元素,返回时head保存头结点元素。
// 若获取失败(队列空),则返回值为false,否则返回值为true。
{
if (SQ_IsEmpty(sq)) {
return 0;
}
else {
head = sq->data[sq->front];
return 1;
}
}
void SQ_Print(SeqQueue* sq)
// 依次打印出队列中的每个元素
{
int i = sq->front;
if (SQ_IsEmpty(sq)) {
printf("queue is emtpy");
return;
}
printf("队列中元素为:");
for (i = sq->front; i != sq->rear; i = (i + 1) % sq->MAXSIZE) {
printf("%d ", sq->data[i]);
}
printf("\n");
}
int main() {
SeqQueue* sq = SQ_Create(5);
int i = 0;
// 依次入队4,5,6
SQ_In(sq, 4);
SQ_In(sq, 5);
SQ_In(sq, 6);
SQ_Print(sq);
// 队头元素出队并打印出队元素
SQ_Out(sq, i);
printf("%d\n", i);
SQ_Out(sq, i);
printf("%d\n", i);
// 出队后队列
SQ_Print(sq);
SQ_Free(sq);
return 0;
}
2.2.队列的链式存储结构
函数中修改指针指向传入二级指针。
rear->next -- 头结点( 无有效数据 ) 但当列表中只有一个数据时,出队要把rear指针指向头结点,否则rear将指向被释放的结点。
#include <stdio.h>
#include <stdlib.h>
typedef int T;
typedef struct node{
T data;
struct node* next;
}QNode;
QNode* CLQ_Create()
// 创建一个队列
{
QNode* rear = (QNode*)malloc(sizeof(QNode));
rear->data = 0;
rear->next = rear;
return rear;
}
int CLQ_IsEmpty(QNode* rear)
// 判断队列是否为空
{
if (rear->next == rear) {
return 1;
}
return 0;
}
int CLQ_Length(QNode* rear)
// 返回队列长度,rear指向尾结点
{
int length = 0;
QNode* p = rear->next->next; // rear->next->next第一个存有效数据的结点(队头)
while (p != rear->next) { // rear->next头结点
length++;
p = p->next;
}
return length;
}
void CLQ_In(QNode** rear, T x)
// 入队列, 新结点加入链表尾部。rear指向尾结点
{
QNode* p = (QNode*)malloc(sizeof(QNode));
p->data = x;
p->next = (*rear)->next;
(*rear)->next = p;
(*rear) = p;
}
int CLQ_Out(QNode** rear, T* item)
// 出队列。空队列时,返回值为false。rear指向尾结点
{
if (CLQ_IsEmpty(*rear)) {
return 0;
}
QNode* head = (*rear)->next;
QNode* p = head->next;
*item = p->data;
head->next = p->next;
if (head->next == head) {
*rear = head;
}
free(p);
return 1;
}
void CLQ_MakeEmpty(QNode** rear)
// rear指向尾结点
// 将队列变为空队列
{
T item;
while (!CLQ_IsEmpty(*rear) && *rear != NULL)
CLQ_Out(rear, &item);
}
void CLQ_Free(QNode** rear)
// rear指向尾结点
{
CLQ_MakeEmpty(rear);
free(*rear);
*rear = NULL;
}
int CLQ_Head(QNode* rear, T* item)
// rear指向尾结点。
// 获取队列头。空队列时返回值为false。
{
if (CLQ_IsEmpty(rear))
return 0;
*item = rear->next->next->data;
return 1;
}
void CLQ_Print(QNode* rear)
// 打印队列。
{
if (CLQ_IsEmpty(rear)) {
printf("The queue is: empty. \n");
return;
}
QNode* node = rear->next->next;
do {
printf("%d ", node->data);
node = node->next;
} while (node != rear->next);
printf("\n");
}
int main() {
QNode* rear = CLQ_Create();
T item = 0;
CLQ_In(&rear, 4);
CLQ_In(&rear, 5);
CLQ_In(&rear, 6);
printf("队列元素依次为:");
CLQ_Print(rear);
CLQ_Out(&rear, &item);
printf("出队元素为:%d", item);
CLQ_Free(&rear);
return 0;
}
2.2.离散事件模拟
三、串
串,即字符串( String ),是由零个或多个字符组成的有限序列。一般记作 S = "a1a2...an"。其中,n为串长,n为0时为空串。
字符在主串中的位置:字符在串中的序号。
注意:位序从1开始。
3.1.串的存储结构
顺序存储
a.静态数组实现
#define MAXLEN 100 // 预定义最大串长
typedef struct{
char ch[MAXLEN];
int length; // 真实串长
}SString;
b.动态数组实现
typedef struct{
char* ch;
int length;
}HString;
HString S;
S.ch = (char*)malloc(NAXLEN * sizeof(char)); // 用完手动free
S.length = 0;
链式存储
typedef struct node{
char ch;
struct node* next;
}NString;
typedef struct{
NString* head;
NString* tail;
}LinkedList;
3.2.模式匹配算法
字符串模式匹配就是找到主串中第一个与模式串匹配的子串的位置。
朴素模式匹配( BF算法 )
朴素模式匹配是一种直接暴力比较的字符串匹配方法。它的核心思想是:从主串的每一个位置开始,依次与模式串进行逐字符比较,直到匹配成功或主串遍历结束。最坏时间复杂度:O(mn)。
int Index(SSting S, SString T){
int i = 1, j = 1;
while(i<=S.length && j<=T.length){
if(S.ch[i] == T.ch[j]){
++i;++j;
}
else{
i = i-j+2;
j = 1;
}
}
if(j>T.length)
return i-T.length;
else
return 0;
}
KMP算法( 1位下标 )
与传统暴力匹配算法相比,KMP的核心优势在于避免主串指针回溯,通过利用已匹配的前缀信息减少重复比较。最坏时间复杂度:O(m+n)。
int Index_KMP(SString S, SString T, int next[]){
int i = 1,j = 1;
while( j <= T.length && i <= S.length ){
if ( j == 0 || T.data[j] == S.data[i] ){
i++;
j++;
}
else{
j = next[j];
}
}
return j>T.length?(i-T.length):-1;
}
next数组
当模式串与主串匹配失败时,KMP算法不会将主串指针回退,而是通过next数组确定模式串指针应回退的位置。next数组记录了模式串中每个子串的最长公共前后缀长度,用于跳过不可能匹配的字符,从而提高匹配效率。
| 模式串 | a | b | a | b | a | a | a | b |
| 下标 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
| next数组 | 0 | 1 | 1 | 2 | 3 | 4 | 2 | 2 |
比较对应字符时,即已知主串待比较的字符前面的字符与模式串对应字符前的字符相同。
改进的KMP算法
nextval数组
先求next数组,再由next数组求nextval数组。
nextval[1] = 0;
for ( int j = 2;j<=T.length;j++ ){
if(T.ch[j] == T.ch[next[j])
nextval[j] = nextval[next[j]];
else
nextval[j] = next[j];
}
| 模式串 | a | b | a | b | a | a | a | b |
| 下标 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
| next数组 | 0 | 1 | 1 | 2 | 3 | 4 | 2 | 2 |
| nextval数组 | 0 | 1 | 0 | 1 | 0 | 4 | 2 | 1 |
四、数组和广义表
4.1.数组
数组是一个固定长度的存储相同数据类型的数据结构,数组中的元素被存储在一段连续的内存空间中。
二维数组 A[M,N] 中任意元素的存储位置:LOC(aᵢ,ⱼ) = LOC(a₀,₀) + (i*N + j)*L。
LOC(a₀,₀) 为基地址,L为单个数组元素大小。
4.1.1.普通矩阵的存储
用二维数组存储。
注意:矩阵行、列号和数组是从0开始,还是从1开始。
4.1.2.矩阵的压缩存储
压缩存储适用于稀疏矩阵和一些特殊矩阵(如对称矩阵,三对角矩阵),可以节省存储空间。
随机稀疏矩阵的存储方式:三元组顺序表,行逻辑联接的顺序表,十字链表。
对称矩阵的压缩存储
对称矩阵的特点:aᵢ,ⱼ = aⱼ,ᵢ 故只需要存储左下三角或右上三角的元素即可。
N阶对称矩阵若按行优先原则存入一维数组中(i >= j): a₁,₁ --> A[0]
数组大小:n*(n+1)/2
aᵢ,ⱼ --> A[ i*(i-1)/2 + j - 1 ] -1 是由于数组从 0 开始存储
三对角矩阵的压缩存储
又称带状矩阵,其特点:只有 | i - j | <= 1 的位置上有值。
N阶三对角矩阵若按行优先原则存入一维数组中: a₁,₁ --> A[0]
除第一行和最后一行只有2个元素,其余行均含3个元素。
数组大小:3*n-2
aᵢ,ⱼ --> A[ (i-1)*3 - 1 + j - i + 2 - 1 ] = A[ 2i + j - 3 ]
(i-1)*3-1 前i-1的元素数量,j - i + 2 位于第i行的第几个元素,-1 数组从0开始存储。
三元组顺序表
一个三元组中分别存放一个矩阵的元素的行号、列号和值。
| row | column | value |
4.2.广义表
广义表是一种线性存储结构,既可以存储不可再分的元素,也可以存储广义表,记作:LS = (a1,a2,…,an),其中,LS 代表广义表的名称,an 表示广义表存储的数据,广义表中每个 ai 既可以代表单个元素,也可以代表另一个广义表。
广义表表示空表时,其第一个数据称为表头HEAD,剩余数据称为表尾TAIL。
存储结构:tag 标记位、union(atom 或 hp 指针和 tp指针)。tag标记位用于区分该结点存储的是元素(tag = 0)还是子表(tag = 1)。
广义表的长度:广义表中所包含的数据元素的个数,一个子表算作一个数据元素。
广义表的深度:通过观察该表中所包含括号的层数间接得到。表的深度 = 最大子表深度+1。
五、树
树是一个或多个结点的有限集合。存在一个称为根的特定结点,其余的结点被分为n个互不相交的集合,其中每个集合都是一棵树。
5.1.树的基本性质
性质一:树中的所有结点数等于结点的度数之和加一。
性质二:对于度为m的树,第i层上最多有 m^(i-1) 个结点。
性质三:对于度为m,高度为h的树,最多有 (m^h-1)/(m-1) 个结点。
5.2.二叉树
二叉树是度数为2的树。
完全二叉树:深度为k,含有n个结点的二叉树,其结点编号与同深度满二叉树中编号为1至n的结点位置完全对应。( 叶子结点只能在层次最大的两层出现并且对任意结点,若右分支的最大层数为l,则左分支的最大层次为l或l+1)
5.2.1.二叉树的性质
性质一:二叉树的第i层最多有 2^(i-1) 个结点。
性质二:深度为k的二叉树最多有 2^k-1 个结点。
性质三:对于任意非空的二叉树,如果叶子结点的个数为n0,度为2的结点个数为n2,则有n0=n2+1。
5.2.2.二叉树的构造
struct node
{
DataType info ; //存放结点数据
struct node *lchild , *rchild ; //指向左右孩子的指针
};
typedef struct node *BiTree ;
BiTree createBiTree(void)
{
DataType ch;
cin >> ch;
if (ch == '#'){
return NULL;
}
BiTree T = new node;
T->info = ch;
T->lchild = createBiTree();
T->rchild = createBiTree();
return T;
}
5.2.3.二叉树的遍历
已知中序遍历和前(后)序遍历,可以唯一确定一棵二叉树。
前序遍历
根 --> 左子树 --> 右子树。
中序遍历
左子树 --> 根 --> 右子树。
后序遍历
左子树 --> 右子树 --> 根。

层序遍历
从左到右,从上到下。主要用于求解 WPL ( 树的带权路径长度 )。
5.2.4.线索二叉树
线索二叉树的结构
| lchild | ltag | 结点值 | rtag | rchild |
ltag = 1:lchild指向结点的前驱;(等于1时对应的child是线索)
ltag = 0:lchild指向结点的左孩子;
rtag = 1:rchild指向结点的后继;
rtag = 0:rchild指向结点的右孩子。
① 头结点的lchild、rchild分别指向二叉树的根和遍历的最后一个结点;
② 第一个结点的lchild指向头结点;
③ 最后一个结点的rchild指向头结点。
加快查找结点前驱或后继的速度。

c++代码实现
// 线索二叉树结点构造
typedef enum { link, thread } PointerTag;
typedef struct ThreadNode{
char data;
struct ThreadNode *lchild, *rchild;
PointerTag ltag, rtag;
}ThreadNode;
// 中序遍历对二叉树进行线索化
void Threading (ThreadNode *p, ThreadNode **pre){
if (p){
Threading(p->lchild, pre);
if(!p->lchild){
p->ltag = thread;
p->lchild = *pre;
}
if(*pre && !(*pre)->rchild){
(*pre)->rtag = thread;
(*pre)->rchild = p;
}
*pre = p;
Threading(p->rchild, pre);
}
}
// 构建线索二叉树
void CreatThreadTree (ThreadNode *T){
ThreadNode *pre = NULL; // 定义变量记录前驱
if (T != NULL){
Threading(T, &pre);
if (pre != NULL){
pre->rtag = thread;
pre->rchild = NULL;
}
}
}
5.3.森林,树和二叉树之间的转换
树的先根遍历相当于二叉树的前序遍历,后根遍历相当于二叉树的中序遍历。
5.3.1.树和二叉树
树 -> 二叉树
化作二叉树后的结点如果有左孩子,则原来在树中该结点为非叶子结点。
① 加线:在所有兄弟结点间加线
② 去线:对所有结点,只保留它与第一个孩子之间的连线,兄弟结点变成右孩子
③ 层次调整

二叉树 -> 树
① 加线:如果该结点的左孩子存在,把它左孩子的所有右孩子结点作为它的孩子连接起来
② 去线:删除所有结点与右孩子的连线
③ 调整
5.3.2.森林和二叉树
二叉树 -> 森林
从根节点开始,若右孩子存在,则把右孩子结点的连线删除。--> 单个二叉树转化为树
森林 -> 二叉树
第一棵二叉树不动,从第二棵树开始,依次把后一棵树的根节点作为前一棵二叉树根节点的右孩子。
5.4.最优二叉树(哈夫曼树)
哈夫曼树是一种带权路径长度(WPL)最短的二叉树,常用于数据压缩与最优编码。其核心思想是权值越大的节点越靠近根节点,权值越小的节点越远离根节点,从而实现整体路径加权和最小。
#define _CRT_SECURE_NO_WARNINGS // 正常用scanf等老函数,不再报安全错误
#include<stdio.h>
#include<stdlib.h>
typedef struct {
int weight;
int left, right, parent;
}Node;
// 查找权值最小的两个结点
void FindMinWeight(Node T[], int N, int* s1, int* s2) {
int min1 = 0x7fffffff;
int min2 = 0x7fffffff;
for (int i = 0; i < N; i++) {
if (T[i].parent) {
continue;
}
if (min1 > T[i].weight) {
min2 = min1;
min1 = T[i].weight;
*s2 = *s1;
*s1 = i;
}
else if (min2 > T[i].weight) {
min2 = T[i].weight;
*s2 = i;
}
}
T[*s1].parent = 1;
T[*s2].parent = 1;
}
// 构建哈夫曼树
void CreatHuffTree(Node T[], int weight[], int N) {
int s1 = 0;
int s2 = 0;
int total = 2 * N - 1;
// 构建叶子结点
for (int i = 0; i < N; i++) {
T[i].weight = weight[i];
T[i].parent = T[i].left = T[i].right = 0;
}
// 构建非叶子结点
for (int i = N; i < total; i++) {
FindMinWeight(T, i, &s1, &s2);
T[s1].parent = i;
T[s2].parent = i;
T[i].weight = T[s1].weight + T[s2].weight;
T[i].left = s1;
T[i].right = s2;
T[i].parent = 0;
}
}
int getWPL(Node T[], int N) {
int WPL = 0;
for (int i = 0; i < N ; i++) {
int cur = i, depth = 0;
while (T[cur].parent) {
depth++;
cur = T[cur].parent;
}
WPL += T[i].weight * depth;
}
return WPL;
}
int main() {
int N = 0;
printf("输入叶子结点数量:");
scanf("%d", &N);
int* weight = (int*)malloc(sizeof(int)*N);
printf("输入各叶子结点权值:");
for (int i = 0; i < N; i++) {
scanf("%d", &weight[i]);
}
Node* tree = (Node*)malloc(sizeof(Node) * (2*N - 1));
CreatHuffTree(tree, weight, N);
printf("%d", getWPL(tree, N));
free(weight);
free(tree);
return 0;
}
六、图
图是由顶点的有穷非空集合和顶点之间的边的集合构成的。通常记为 G(V,E)。
图分为无向图和有向图,边有箭头的是有向图,箭头指向的方向是弧头,另一边为弧尾,0->1记作<0,1>。
简单路径:不出现相同的顶点的从一个顶点开始到达另一个顶点的顶点序列。

&spm=1001.2101.3001.5002&articleId=159248021&d=1&t=3&u=5f78a581e07b4e1da03a4e78f1ab9c91)
3061

被折叠的 条评论
为什么被折叠?



