C语言—数据结构 故天将降大任于是人也,必先苦其心志,劳其筋骨,饿其体肤,空乏其身,行拂乱其所为
——《孟子·告子章句下·第十五节》
一、数据结构之基础 1.数据结构的三要素
1.若采用顺序存储,则各个数据元素在物理上必须是连续的;若采用非顺序存储,则各个数据元素在物理上可以是离散的。 2.数据的存储结构会影响存储空间分配的方便程度 3数椐的存储结构会影响对数据运算的速度
数据的存储结构有几种 线性
连续存储(数组)
优点:存储速度很快
缺点:插入和删除元素很慢,空间通常是有限的
离散存储(链表)
优点:空间没有限制,插入删除元素很快
缺点:存取速度很慢
2、存储结构
顺序结构 顺序存储,把逻辑上相邻的元素存储在物理位置上也相邻的存储单元中,元素之间的关系由存储单元的邻接关系来体现。
链式存储 链式存储,逻辑上相邻的元素在物理位置上可以不相邻,借助指示元素存储地址的指针来表示元素之间的逻辑关系。(指针链接指针)
索引存储 索引存储。在存储元素信息的同时,还建立附加的索引表。索引表中的每项称为索引项,索引项的一般形式是(关键字,地址)离散的
散列存储 散列存储。根据元素的夭键字直接计算出该元素的存储地址,又称哈希(Hash)存储
3.算法特征: 确定性,可行性,输入,输出
好算法特征: 正确性,可读性,健壮性,高效率与低存储需求(时间,空间复杂度低)
二、链表 1.数组的优缺点: 优点:存储速度快
缺点:需要一个连续很大的内存,插入和删除元素的效率很低
2.链表 优点:插入 删除元素效率高,不需要一个连续的很大内存
缺点:查找某个元素的效率低
3.术语: 首节点 尾节点 头结点: 头结点的数据类型和首节点的类型是一样的 头结点是首结点前面的那个节点 头结点里面不存放有效数据 设置结点是为了方便对链表经行操作 头指针 存放头结点的地址的指针变量 可以通过头指针推算出链表的其他信息 4.链表的定义 n个节点离散分配 彼此通过指针相连接 每个节点只有一个前驱节点和后驱节点 首节点没有前驱节点, 尾节点没有后续节点 typedef struct Node { int date; struct Node * pNext ; }Node,*pNode;
5.分类: 单链表
双链表
每一个节点都有两个指针域
循环链表
能通过任何一个节点找到其他所有节点
非循环链表
6.算法 遍历
查找
清空
销毁
排序
删除节点
插入节点
7.创建带头单链表并遍历输出 郝斌老师代码
#include <stdio.h> #include <stdlib.h> typedef struct Node { int data; struct Node * pNext ; }Node,* pNode; pNode creat_list () ; void traverse_list (pNode pNode) ;int main () { pNode pHead =NULL ; pHead = creat_list(); traverse_list(pHead); return 0 ; } pNode creat_list () { int len; int i; int val; pNode pHead =(pNode) malloc (sizeof (Node)); if (NULL ==pHead) { printf ("内存分配失败" ); exit (-1 ); } pNode pTail=pHead; pTail->pNext=NULL ; printf ("请输入链表的节点个数:len =" ); scanf ("%d" ,&len); for ( i = 0 ; i < len; ++i) { printf ("请输入第%d个节点的值" ,i+1 ); scanf ("%d" ,&val); pNode pNew =(pNode) malloc (sizeof (Node)); if (NULL ==pNew) { printf ("内存分配失败" ); exit (-1 ); } pNew->data=val; pTail->pNext=pNew; pNew->pNext=NULL ; pTail=pNew; } return pHead ; } void traverse_list (pNode pHead) { pNode p=pHead->pNext; while (NULL !=p) { printf ("%d " ,p->data); p=p->pNext; } printf ("\n" ); }
8.※链表的基本操作※ 郝斌老师代码
#include <stdio.h> #include <stdlib.h> #include <stdbool.h> typedef struct Node { int data; struct Node * pNext ; }Node,* pNode; pNode creat_list () ; void traverse_list (pNode pNode) ;bool is_empty (pNode pHead) ;int length_list (pNode) ;bool insert_list (pNode,int ,int ) ;bool delete_list (pNode pHead,int pos,int *pval) ;void sort_list (pNode) ;int main () { pNode pHead =NULL ; pHead = creat_list(); traverse_list(pHead); return 0 ; } pNode creat_list () { int len; int i; int val; pNode pHead =(pNode) malloc (sizeof (Node)); if (NULL ==pHead) { printf ("内存分配失败" ); exit (-1 ); } pNode pTail=pHead; pTail->pNext=NULL ; printf ("请输入链表的节点个数:len =" ); scanf ("%d" ,&len); for ( i = 0 ; i < len; ++i) { printf ("请输入第%d个节点的值" ,i+1 ); scanf ("%d" ,&val); pNode pNew =(pNode) malloc (sizeof (Node)); if (NULL ==pNew) { printf ("内存分配失败" ); exit (-1 ); } pNew->data=val; pTail->pNext=pNew; pNew->pNext=NULL ; pTail=pNew; } return pHead ; } void traverse_list (pNode pHead) { pNode p=pHead->pNext; while (NULL !=p) { printf ("%d " ,p->data); p=p->pNext; } printf ("\n" ); } bool is_empty (pNode pHead) { if (NULL ==pHead->pNext) { return true ; } else return false ; } int length_list (pNode pHead) { pNode p=pHead->pNext; int len =0 ; while (NULL !=p) { ++len; p=p->pNext; } return len; } void sort_list (pNode pHead) { int i,j,t;int len = length_list(pHead);pNode p,q; for ( i = 0 ,p=pHead->pNext; i <len-1 ; ++i,p=p->pNext) { for (j = i+1 ,q=p->pNext; j <len ; ++j,q=q->pNext) { if (p->data > q->data) { t=p->data; p->data=q->data; q->data=t; } } } return ; } bool insert_list (pNode pHead,int pos,int val) { int i=0 ; pNode p=pHead; while (NULL !=p && i<pos-1 ) { p=p->pNext; i++; } if (i>pos-1 || NULL ==p) { return false ; } pNode pNew =(pNode) malloc (sizeof (Node)); if (NULL ==pNew) { printf ("动态内存分配失败\n" ); exit (-1 ); } pNew ->data=val; pNode q=p->pNext; p->pNext=pNew; pNew->pNext=q; return true ; } bool delete_list (pNode pHead,int pos,int * pval) { int i=0 ; pNode p=pHead; while (NULL !=p->pNext && i<pos-1 ) { p=p->pNext; i++; } if (i>pos-1 || NULL ==p->pNext) { return false ; } pNode q=p->pNext; *pval=q->data; p->pNext=p->pNext->pNext; free (q); q=NULL ; return true ; }
9.李阳的线性表 #include <stdio.h> #include <malloc.h> #define MAXNUM 100 typedef int DateType;typedef struct { DateType data[MAXNUM]; int length; }SqList; void initList (SqList *L) { L->length=0 ; } int ListInsert (SqList *L,int i,DateType d) { if (i<1 || i>L->length+1 ) { return 0 ; } if (L->length>MAXNUM) { return 0 ; } for (int k = L->length; k >=i ; k--) { L->data[k]=L->data[k-1 ]; } L->data[i-1 ]=d; L->length++; return 1 ; } int ListDelete (SqList *L,int i,DateType *d) { if (i<1 ||i>L->length) { return 0 ; } *d=L->data[i-1 ]; for (int k=i;k<L->length;k++) { L->data[k-1 ]= L->data[k]; } L->length--; return 1 ; } void TraverseList (SqList L) { for (int i = 0 ; i < L.length; ++i) { printf ("%d " ,L.data[i]); } printf ("\n" ); } int ListLength (SqList L) { return L.length; } int ListUpdate (SqList *L,int i,DateType e) { if (i<0 ||i>L->length) { return 0 ; } L->data[i-1 ]=e; return 1 ; } int ListGit (SqList L,int i,DateType *x) { if (i<0 ||i>L.length) { return 0 ; } *x=L.data[i]; return 1 ; } int LocateElem (SqList L, DateType e) { for (int i = 0 ; i < L.length; i++) { if (L.data[i] == e) { return i + 1 ; } } return 0 ; } int main () { SqList L; initList(&L); ListInsert(&L,1 ,1 ); ListInsert(&L,2 ,2 ); ListInsert(&L,3 ,3 ); ListInsert(&L,4 ,4 ); ListInsert(&L,5 ,5 ); ListInsert(&L,6 ,6 ); TraverseList(L); DateType d; ListDelete(&L,2 ,&d); printf ("删除的元素是:%d " ,d); printf ("\n" ); TraverseList(L); }
10.李阳的带头单链表 #include <stdio.h> #include <stdlib.h> typedef struct Node { int data; struct Node * next ; }Node; Node * initList () { Node * head=(Node *) malloc (sizeof (Node)); if (!head) { printf ("动态内存分配失败!" ); exit (1 ); } head->next=NULL ; return head; } void instertATEnd (Node * head ,int date) { Node * newNode=(Node *) malloc (sizeof (Node)); if (!newNode) { printf ("创建失败" ); exit (1 ); } newNode ->data=date; newNode->next=NULL ; Node *temp=head; while (temp->next!=NULL ) { temp=temp->next; } temp->next=newNode; } void insertATHead (Node * head,int data) { Node * newNode=(Node *) malloc (sizeof (Node)); if (!newNode) { printf ("创建失败" ); exit (1 ); } newNode->data=data; newNode->next=head->next; head->next=newNode; } void printList (Node * head) { Node * temp=head->next; while (temp!=NULL ) { printf ("%d " ,temp->data); temp=temp->next; } printf ("\n" ); } void deleteNode (Node * head, int data) { Node * temp=head; while (temp->next!=NULL &&temp->next->data!=data) { temp=temp->next; } if (temp->next!=NULL ) { Node * current=temp->next; temp->next=current->next; free (current); } else { printf ("没找到哦" ); } } int getElement (Node* head, int index) { Node* temp = head; int count = 0 ; while (temp != NULL && count < index) { temp = temp->next; count++; } if (temp == NULL ) { printf ("索引 %d 越界\n" , index); exit (1 ); } return temp->data; } int getListLength (Node* head) { int length = 0 ; Node* current = head->next; while (current != NULL ) { length++; current = current->next; } return length; } void freeList (Node* head) { Node* current = head->next; Node* nextNode; while (current != NULL ) { nextNode = current->next; free (current); current = nextNode; } free (head); } int insertList (Node * head,int i,int data) { Node * newNode =(Node *) malloc (sizeof (Node)); if (!newNode) { printf ("内存分配失败" ); return -1 ; } Node * temp=head; int index=-1 ; if (i == 0 ) { newNode->next = head; newNode->data = data; head = newNode; return 1 ; } while (temp->next!=NULL && index<i-1 ) { temp=temp->next; index++; } if (index != i - 1 ) { free (newNode); printf ("插入位置超出链表长度\n" ); return -1 ; } newNode->data=data; newNode->next=temp->next; temp->next=newNode; return 1 ; } int deleteList (Node* head,int i,int *data) { if (head->next == NULL ) { printf ("链表为空,无法删除\n" ); return -1 ; } Node * temp=head; int index=-1 ; while (temp->next!=NULL && index<i-1 ) { temp=temp->next; index++; } if (index!=i-1 ) { printf ("删除有误" ); return -1 ; } Node * current=temp->next; *data=current->data; temp->next=current->next; free (current); return 1 ; } int main () { Node * head=initList(); instertATEnd(head,1 ); instertATEnd(head,2 ); instertATEnd(head,3 ); instertATEnd(head,4 ); printf ("尾插法" ); printList(head); printf ("----------------------" ); printf ("头插法\n" ); insertATHead(head,5 ); insertATHead(head,4 ); printList(head); deleteNode(head,2 ); printList(head); int length = getListLength(head); printf ("Linked list length: %d\n" , length); freeList(head); return 0 ; }
三、栈 1.定义 一种可以实现“先进后出”的存储结构
栈类似于一个箱子,先放进去的后拿出来,后进去的先拿出来
2.分类 静态栈
动态栈
3.算法 出栈
压栈
郝斌老师代码
#include <stdio.h> #include <stdio.h> #include <malloc.h> #include <stdbool.h> typedef struct Node { int data; struct Node *pNext ; }Node,*pNode; typedef struct Stack { pNode pTop; pNode pBottom; }sTack,*pStack; void initStack (pStack) ;void pushStack (pStack pS,int val) ;void traverse (pStack) ;bool popStack (pStack,int *) ;bool empty (pStack pS) ;void clear (pStack pS) ;int main () { int val; sTack S; initStack(&S); pushStack(&S,1 ); pushStack(&S,2 ); pushStack(&S,3 ); pushStack(&S,4 ); traverse(&S); clear(&S); if ( popStack(&S,&val)) printf ("出栈成功,出栈的元素是:%d\n" ,val); else printf ("出栈失败\n" ); traverse(&S); return 0 ; } void initStack (pStack pS) { pS->pTop=(pNode)malloc (sizeof (Node)); if (NULL ==pS->pTop) { printf ("内存分配失败\n" ); exit (-1 ); } else { pS->pBottom=pS->pTop; pS->pTop->pNext=NULL ; } } void pushStack (pStack pS,int val) { pNode pNew=(pNode) malloc (sizeof (Node)); pNew->data=val; pNew->pNext=pS->pTop; pS->pTop=pNew; } void traverse (pStack pS) { pNode p=pS->pTop; while (p!=pS->pBottom) { printf ("%d" ,p->data); p=p->pNext; } printf ("\n" ); } bool empty (pStack pS) { if (pS->pTop==pS->pBottom) return true ; else return false ; } bool popStack (pStack pS,int * pVal) { if (empty(pS)) { return false ; } else { pNode r=pS->pTop; *pVal=r->data; pS->pTop=r->pNext; free (r); r=NULL ; return true ; } } void clear (pStack pS) { if (empty(pS)) { return ; } else { pNode p=pS->pTop; pNode q=NULL ; while (p!=pS->pBottom) { q=p->pNext; free (p); p=q; } pS->pTop=pS->pBottom; } }
4.应用 函数调用
中断
表达式求值
内存分配
缓存处理
迷宫
5.栈的表达式求值—后缀表达式(逆波兰表达式) 1.例如:1+2*3
后缀表达式为 1 2 3 * +
遇到一个运算符他会在前面两个数字间运算
2 *3 然后向后移到+ 6+1 2、(a + b) * (c - (d / e)) + f
3.、a * (b + c) - (d / e)
4、a + b * (c - d / e) ^ f
总结
1. 操作数和运算符 操作数(如变量和常数)直接添加到输出列表中。 运算符则需要根据优先级和括号来决定是否入栈、弹出或保持在栈中。 2. 运算符优先级 运算符的优先级从高到低一般为: * 和 / (乘法和除法)优先级高 + 和 - (加法和减法)优先级低 在遇到相同优先级的运算符时,按照左结合性处理(从左到右)。 3. 括号的处理 遇到左括号 ( 时,直接入栈。 遇到右括号 ) 时,弹出栈中的运算符到输出,直到遇到对应的左括号为止,左括号被丢弃。 4. 栈的使用 当一个运算符被处理时,检查栈顶运算符的优先级: 如果栈顶运算符的优先级高于或等于当前运算符,则弹出栈顶运算符到输出。 否则,将当前运算符入栈。 5. 表达式结束时的处理 当整个中序表达式处理完毕后,仍有运算符在栈中时,将它们全部弹出到输出。 6. 后缀表达式的特征 后缀表达式不需要括号,因为运算符的顺序和位置已经明确了运算的优先级和关联性。
当一个运算符被处理时,检查栈顶运算符的优先级: 如果栈顶运算符的优先级高于或等于当前运算符,则弹出栈顶运算符到输出。 否则,将当前运算符入栈。
总结:
栈底部的运算符永远是最低的 遇到括号先输出括号里的运算符,但不保留括号 当有刚入栈的运算符小于或等于当前栈中的运算符,那直接将栈出到小于此运算符的位置 来看个小例子吧: 1.当栈低为+或-时,来了个*或/时可以留着栈中,当此时又来了一个新运算符,假设为-,此时栈中的为+和,那么将\ 和+直接出栈,-再放入栈中 2.假设栈底运算符为或者/,当入栈为+或-时,那么将\ 或\出栈,将+或-压入栈底部 3.当栈低为+和时,此时入栈了一个/那么将\ 出栈,+留在栈中,/也压入栈中 特别注意:* / ^的运算符是在两个运算的字母后的
例题:
eg:1. (A + B) * (C - D) / E ^ F 遇到括号左括号进栈A输出+进栈B输出,扫描到右括号+出栈,此时为AB+ *入栈,左括号入栈C输出-入栈D输出右括号出栈,此时变为AB+CD-,栈低为* 扫描/入栈,因为/的优先级不大于*,因此*出栈,此时变为AB+CD-*,栈低为/ E输出,^进栈^的优先级不大于/要出栈,但是因为我们的运算符要相匹配,因此F输出在依次输出栈/^,这里E F ^ 表示 E 和 F 进行指数运算。/ 表示乘法结果除以 E F ^ 的结果 AB+CD-*EF/^ 2. A + B * (C ^ D - E) ^ F / G 输出A,+进栈,输出B,此时为AB,+在栈里面 扫描左括号入栈,C输出,^进栈D输出,-进栈,因为我们的-优先级低于^,所以^的位置上移动,出栈,E输出扫描到又括号^出栈,-出栈,栈低为+和*,此时为ABCD^E- ^入栈,因为^的优先级不大于*,所以*出栈,^入栈,栈里为+^,F输出,/进栈,因为FG是个组合,所以输出*FG/^ 结果是ABCD^E-*FG/^
6.后缀表达式转中缀 我们学完了中缀转后缀下面来试一试后缀表达式转中缀吧!
我们先来看题吧
AB+CD-*EF/^ 首先我们的ab进栈,这时我们扫描到运算符+,这时候就变成a+b,然后入栈(遇到运算符要与他的前两位进行计算) cd进栈,扫描到运算符-,就变成c-d然后入栈,此时栈里存放的是a+b 和c-d 然后扫描到运算符* ,就变成 a+b * c-d ,因为我们左右两边是个整体,所以要加括号(a+b) * (c-d),然后再入栈 EF进栈,扫描到/,就变成E/F,此时栈里存放的是(a+b) * (c-d) 和 (E/F) 扫描到^ 就变成 (a+b) * (c-d) ^ (E/F) 本题结束(●'◡'●)
由此我们可以反推出 :
字母总是两两存在的因此在中序转后缀需要考虑组合
后缀转中缀的是运算符之前的两位数字/字母进行计算
7.李阳的栈 #include <stdio.h> #include <stdlib.h> typedef struct Node { int data; struct Node * next ; }Node; typedef struct LinkedStack { Node * top; } LinkedStack; LinkedStack * initStack () { LinkedStack * stack =(LinkedStack *) malloc (sizeof (LinkedStack)); stack ->top=NULL ; return stack ; } int isEmpty (LinkedStack * stack ) { return stack ->top==NULL ; } void push (LinkedStack* stack ,int data) { Node * newNode=(Node *) malloc (sizeof (Node)); newNode->data=data; newNode->next=stack ->top; stack ->top=newNode; } int pop (LinkedStack * stack ) { if (isEmpty(stack )) { printf ("栈是空的" ); return -1 ; } Node * temp=stack ->top; int popData=temp->data; stack ->top=temp->next; free (temp); return popData; } void printStack (LinkedStack *stack ) { Node *temp=stack ->top; while (temp!=NULL ) { printf ("%d " ,temp->data); temp=temp->next; } printf ("NULL\n" ); } int StackTop (LinkedStack*stack ) { Node * temp=stack ->top; if (temp==NULL ) { printf ("栈为空" ); return 0 ; } int topdata=temp->data; return topdata; } void freeStack (LinkedStack * stack ) { Node * temp=stack ->top; Node *nextNode; while (temp!=NULL ) { nextNode=temp->next; free (temp); temp=nextNode; } free (stack ); } int main () { LinkedStack *stack =initStack(); push(stack ,1 ); push(stack ,2 ); push(stack ,3 ); push(stack ,4 ); printStack(stack ); printf ("\n" ); int a= StackTop(stack ); printf ("栈顶元素是:%d " ,a); printf ("\n" ); int b= pop(stack ); printf ("%d出栈了" ,a); printf ("\n" ); printStack(stack ); freeStack(stack ); }
四、队列 1、定义 一种可以实现“先进先出”的存储结构—排队买票进站
2.分类 链式队列(链表)
静态队列(数组)
静态队列通常都必须是循环队列
3.代码 郝斌老师代码
#include <stdio.h> #include <malloc.h> #include <stdbool.h> typedef struct Queue { int *pBase; int front; int rear; } QUEUE; void init (QUEUE *) ;bool en_queue (QUEUE *,int ) ;void traverse_queue (QUEUE *) ;bool full_queue (QUEUE *) ;bool out_queue (QUEUE *,int *pVal) ;bool emput_queue (QUEUE *) ;int main () { QUEUE Q; int val; init(&Q); en_queue(&Q,1 ); en_queue(&Q,2 ); en_queue(&Q,3 ); en_queue(&Q,4 ); en_queue(&Q,5 ); traverse_queue(&Q); if (out_queue(&Q, &val)) { printf ("出队成功: %d\n" , val); } else { printf ("出队失败\n" ); } traverse_queue(&Q); return 0 ; } void init (QUEUE *pQ) { pQ->pBase=(int *) malloc (sizeof (int )*6 ); pQ->front=pQ->rear=0 ; } bool en_queue (QUEUE *pQ,int val) { if (full_queue(pQ)) { printf ("队列满啦" ); return false ; } else { pQ->pBase[pQ->rear]=val; pQ->rear=(pQ->rear+1 )%6 ; return true ; } } bool full_queue (QUEUE *pQ) { if ((pQ->rear+1 )%6 ==pQ->front) return true ; else return false ; } void traverse_queue (QUEUE *pQ) { int i=pQ->front; while ((i!=pQ->rear)) { printf ("%d" ,pQ->pBase[i]); i=(i+1 )%6 ; } printf ("\n" ); } bool emput_queue (QUEUE * pQ) { if (pQ->front==pQ->rear) { return true ; } else return false ; } bool out_queue (QUEUE * pQ,int *pVal) { if (emput_queue(pQ)) { printf ("表是空的" ); return false ; } else { *pVal=pQ->pBase[pQ->front]; pQ->front=(pQ->front+1 )%6 ; return true ; } }
4.队列的操作 所有和时间有关的操作
5.李阳的队列 #include <stdio.h> #include <stdlib.h> typedef struct Node { int data; struct Node * next ; } Node; typedef struct LinkedQueue { Node* front; Node* rear; } LinkedQueue; LinkedQueue * initQueue () { LinkedQueue * queue =(LinkedQueue*) malloc (sizeof (LinkedQueue)); queue ->front=queue ->rear=NULL ; return queue ; } int isEmpty (LinkedQueue* queue ) { return queue ->front == NULL ; } void enqueue (LinkedQueue * queue ,int data) { Node *newNode=(Node* ) malloc (sizeof (Node)); newNode->data=data; newNode->next=NULL ; if (isEmpty(queue )) { queue ->front=queue ->rear=newNode; } else { queue ->rear->next=newNode; queue ->rear=newNode; } } int dequeue (LinkedQueue * queue ) { if (isEmpty(queue )) { printf ("队列是空的" ); return -1 ; } Node *temp=queue ->front; int deque =temp->data; queue ->front=queue ->front->next; if (queue ->front==NULL ){ queue ->rear=NULL ; } free (temp); return deque ; } void printQueue (LinkedQueue*queue ) { Node *temp=queue ->front; while (temp!=NULL ) { printf ("%d " ,temp->data); temp=temp->next; } printf ("NUll\n" ); } void freeQueue (LinkedQueue*queue ) { Node * temp=queue ->front; Node * nextNode; while (temp!=NULL ) { nextNode=temp->next; free (temp); temp=nextNode; } free (queue ); } int getFront (LinkedQueue*queue ) { if (isEmpty(queue )) { printf ("队列为空" ); return -1 ; } else { int temp=queue ->front->data; return temp; } } int main () { LinkedQueue* queue = initQueue(); enqueue(queue , 10 ); enqueue(queue , 20 ); enqueue(queue , 30 ); printQueue(queue ); printf ("\n" ); int temp = getFront(queue ); printf ("队头%d \n" ,temp); printf ("Dequeued: %d\n" , dequeue(queue )); printf ("Dequeued: %d\n" , dequeue(queue )); printQueue(queue ); freeQueue(queue ); return 0 ; }
6.李阳的循环队列 #include <stdio.h> #include <stdlib.h> #include <stdbool.h> typedef struct { int *data; int max_size; int front; int rear; int size; } CircularQueue; CircularQueue* create_circular_queue (int max_size) { CircularQueue *queue = (CircularQueue*)malloc (sizeof (CircularQueue)); if (!queue ) { printf ("内存分配失败\n" ); exit (1 ); } queue ->max_size = max_size; queue ->data = (int *)malloc (max_size * sizeof (int )); if (!queue ->data) { printf ("内存分配失败\n" ); free (queue ); exit (1 ); } queue ->front = -1 ; queue ->rear = -1 ; queue ->size = 0 ; return queue ; } bool is_empty (CircularQueue *queue ) { return queue ->size == 0 ; } bool is_full (CircularQueue *queue ) { return queue ->size == queue ->max_size; } bool enqueue (CircularQueue *queue , int value) { if (is_full(queue )) { printf ("队列已满,无法入队\n" ); return false ; } if (is_empty(queue )) { queue ->front = 0 ; } queue ->rear = (queue ->rear + 1 ) % queue ->max_size; queue ->data[queue ->rear] = value; queue ->size++; return true ; } bool dequeue (CircularQueue *queue , int *value) { if (is_empty(queue )) { printf ("队列为空,无法出队\n" ); return false ; } *value = queue ->data[queue ->front]; if (queue ->front == queue ->rear) { queue ->front = -1 ; queue ->rear = -1 ; } else { queue ->front = (queue ->front + 1 ) % queue ->max_size; } queue ->size--; return true ; } void print_queue (CircularQueue *queue ) { if (is_empty(queue )) { printf ("队列为空\n" ); return ; } int i = queue ->front; for (int count = 0 ; count < queue ->size; count++) { printf ("%d " , queue ->data[i]); i = (i + 1 ) % queue ->max_size; } printf ("\n" ); } void free_circular_queue (CircularQueue *queue ) { if (queue ) { if (queue ->data) { free (queue ->data); } free (queue ); } } int main () { CircularQueue *queue = create_circular_queue(5 ); enqueue(queue , 10 ); enqueue(queue , 20 ); enqueue(queue , 30 ); enqueue(queue , 40 ); print_queue(queue ); int value; dequeue(queue , &value); printf ("出队元素: %d\n" , value); print_queue(queue ); enqueue(queue , 50 ); enqueue(queue , 60 ); print_queue(queue ); free_circular_queue(queue ); return 0 ; }
五、递归
1.简单应用—阶层 #include <stdio.h> #include <malloc.h> #include <stdbool.h> long f (long n) { if (1 ==n) { return 1 ; } else { return f(n-1 )*n; } } int main () { long a,b; printf ("请输入要求的阶层" ); scanf ("%d" ,&a); b= f(a); printf ("%ld 的阶层是 %ld" ,a,b); } long sum (long n) { if (n==1 ) { return 1 ; } else { return sum(n-1 )+n; } }
2.定义,要求,意义 ①定义: 一个函数间接或直接调用自己
②递归需要满足的三个条件 递归必须要有一个明确的终止条件 该函数所处理的数据规模必须在递减 这个转化必须是可解的 ③意义 优点:易于理解
缺点:速度慢,存储空间大
④循环 : 不易理解,速度快,存储空间小
六、串、数组和广义表 一、串(string) 0个或多个任意字符组成的有限序列
1.子串的定义 一个串中任意个连续字符组成的子序列(含空串)称为该串的子串
例如:“abcd”的子串有
“ ”,“a”,”ab”,”abc”,”abcd”
真子串 :是指不包含自身的所有子串
主串 :包含子串的串相应的称为主串
字符位置 :字符在序列中的序号为该字符串中的位置
子串位置 :子串第一个字符在主串中的位置
空格串: 由一个或多个空格组成的串,与空串不同
串相等: 当且仅当两个串的长度相等并且各个对应立置上的字符都相同时,这两个串才是相等的。
2.串的类型定义、存储结构及运算 顺序存储结构-顺序串
链式存储结构-链式串
3.串的表示 顺序存储 #define MAXLEN 255 typedef struct { char ch[MAXLEN+1 ]; int length; }SString;
链式存储 #define CHUNKSIZE 80 typedef struct Chunk { char ch[CHUNKSIZE]; struct Chunk *next ; } Chunk; typedef struct { Chunk *head; Chunk *tail; int curlen; } LString;
4.串的模式匹配 1.BF算法(穷举法) int index_BF (SString 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 ; }
2.※※※ KMP 算法
void get_next (SString T, int next[]) { int i = 1 ; int j = 0 ; next[1 ] = 0 ; while (i < T.length) { if (j == 0 || T.ch[i] == T.ch[j]) { i++; j++; next[i] = j; } else { j = next[j - 1 ]; } } } int Index_KMP (SString S, SString T, int next[]) { int i = 1 ; int j = 1 ; while (i <= S.length && j <= T.length) { if (j == -1 || S.ch[i] == T.ch[j]) { i++; j++; } else { j = next[j]; } } if (j == T.length) return i - T.length; else return -1 ; } }
3.KMP算法改进
void get_nextval (SString T, int nextval[]) { int i = 1 ; int j = 0 ; nextval[1 ] = 0 ; while (i < T.length) { if (j == 0 || T.ch[i] == T.ch[j]) { i++; j++; if (T.ch[i]!=T.ch[j]) nextval[i]=j; else nextval[i]=nextval[j]; } else { j = nextval[j]; } } }
二、数组 1.定义 按照一点格式排列起来的具有相同的数据元素集合
一维数组 :若线性表中的元素为非结构的简单元素,则称为一维数组(线性结构,定长的线性表)
二维数组 :若一维数组中的元素又是一维数组结构,则称为二维数组
二维数组逻辑结构:
非线性结构: 每一个元素既在一个行表中,又在一个列表中 线性结构:该线性表的每一个元素也是一个定长的线性表 数组特点 :结构固定—定义后,维度和为界不再改变
结论 :
线性表结构是数组的一个特例 而数组结构又是线性表结构的扩展 基本操作 :除了结构的初始化和销毁之外,只有取元素和修改元素值的操作
一般采用顺序结构来表示数组
2.存储位置
数组元素a[i] [j] 的存储位置是 LOC(i,j)=LOC(0,0)+n i+j L(L是存储每个元素所需要L个存储单元)
3.压缩存储 什么是压缩存储? 若多个数据元素的值都相同,则只分配一个元素值的存储空间,且零元素不占存储空间 什么样的矩阵能够压缩? 一些特殊矩阵,如:对称矩阵,对角矩阵,三角矩阵,稀疏矩阵等。 什么叫稀疏矩阵? 矩阵中非零元素的个数较少(一般小于5%) 1.对称矩阵 [特点] 沿着对角线对称
在nxn的矩阵a中,满足如下性质: aij=aji (1 ≤i, j <n)
[存储方法] 只存储下(或者上)三角(包括主对角线)的数据元素。共占用n(n+1)/2个元素空间。
2.三角矩阵 [特点] 对角线以下(或者以上)的数据元素(不包括对角线)全部为常数co
[存储方法] 重复元素c共享一个元素存储空间,共占用n(n+1)/2+1个元素空间: sa[1.. n(n+1)/2+1]
3.对角矩阵(带状矩阵) [特点]在nxn的方阵中,所有非零元素都集中在以主对角线为中心的带状区域中,区域外的值全为0,则称为对角矩阵。常见的有三对角矩阵、五对角矩阵、七对角矩阵等。
4.稀疏矩阵 稀疏矩阵:设在mxn的矩阵中有t个非零元素。 令ɸ= t/(mxn) 当ɸ≤0.05时称为稀疏矩阵。
三元组顺序表又称有序的双下标法。
三元组顺序表的优点:非零元在表中按行序有序存储,因此便于进
行依行顺序处理的矩阵运算。
三元组顺序表的缺点:不能随机存取。若按行号存取某一行中的非
零元,则需从头开始进行查找。
十字链表 优点:它能够灵活地插入因运算而产生的新的非零元素,删除因运算而产生的新的零元素,实现矩阵的各种运算
三.广义表 1.定义 广义表 (又称列表Lists)是n≧0个元素 a0,a1…..an-1的有限序列,其中每一个ai或者是原子,或者是一个广义表
广义表通常记作:Ls=(a1, a2,..,an)
其中:LS为表名,n为表的长度,每一个ai为表的元素
习惯上,一般用大写字母 表示广义表 ,小写字母 表示原子。
表头: 若LS非空(n≥1),则其第一个元素a1就是表头。 记作head(LS) = a1。注 :表头可以是原子,也可以是子表。
表尾 :除表头 之外的其它元素 组成的表 。
记作tail(LS) = (a2, …. an)。
注:表尾不是最后一个元素,而是一个子表。
2.性质 (1)广义表中的数据元素有相对次序;一个直接前驱和一个直接后驱
(2)广义表的长度 定义为最外层所包含元素的个数;
如: C=(a (b, q))是长度为2的广义表。
(3)广义表的深度 定义为该广义表展开后所含括号的重数 ;
A=(b ,c)的深度为1,B=(A,d)的深度为2,C=(f, B,h)的深度为3。
注意 :“原子”的深度为0;“空表”的深度为1。
广义表可以为其他广义表共享 ,如:广义表B就共享了广义表A。在b中不必列出A的值,而是通过名称来引用,B=(A)
广义表可以是一个递归的表。如:F=(a,F=(a,(a,(a,…)))
注意:递归表的深度是无穷值,长度是有限值,这里长度是2
广义表是多层次结构,广义表的元素可以是单元素,也可以是子表,而子表的元素还可以是子表,….。 可以用图形象地表示。 例:D=(E,F)其中:E=(a, (b,c))F=(d,(e)) 3.广义表和线性表的区别 广义表可以看成是线性表的推广,线性表是广义表的特例。 广义表的结构相当灵活,在某种前提下,它可以兼容线性表、数组、树和有向图等各种常用的数据结构。 当二维数组的每行(或每列)作为子表处理时,二维数组即为一个广义表。 另外,树和有向图也可以用广义表来表示。 由于广义表不仅集中了线性表、数组、树和有向图等常见数据结构的特点,而且可有效地利用存储空间,因此在计算机的许多应用领域都有成功使用广义表的实例。 4.广义表的基本运算
案例分析
代码实现
七、树 1.树的定义
树的定义 树 (Tree)是n (n≥0)个结点的有限集。(递归) 若n =0,称为空树 ; 若n >0,则它满足如下两个条件: (1)有且仅有一个 特定的称为根 (Root)的结点; (2)其余结点 可分为m (m≥0)个互不相交的有限集 T1,T2,T3,…Tm,其中每一个集合本身又是一棵树,并称为根的子树 (SubTree)。
树的基本术语 结点 :数据元素以及指向树的分支
根结点 :非空树中无前驱点的结点
结点的度 :结点拥有的子树数
eg: A 3,B 2,F 0
树的度 : 数内各结点的度的最大值
叶子结点 :终端结点(度为0)
eg: k L G M J
分支结点 :非终端结点
eg: B,C,D…
内部结点 :根节点以外的分支结点称为内部结点
孩子结点 :结点的子树的根称为孩子
双亲结点 :孩子结点的前驱
eg: BCD是A的孩子结点,A是bcd的双亲结点
兄弟结点 :同级的结点
祖先结点 :从根到该结点所经分支上的所有结点
eg: M的祖先结点为H,D,A
子孙结点 :从某结点为根的子树的任意结点
eg: D的孙子有HM
树的深度 :树中结点的最大层次
有序树 :树中的各子树从左至右有次序(最左边为第一个孩子)
无序树 :树中结点的各子树无次序
森林 :是m(m>=0)棵互不相交的树的集合
树一定是森林,森林不一定是树
树结构和线性结构的比较
2.二叉树的定义
定义 二叉树是n(n>=0)个结点的有限集,它或者是空集(n= 0),或者由一个根结点及两棵互不相交的分别称作这个根的左子树和右子树的二叉树组成。
特点 每个结点最多有俩孩子(二叉树中不存在度大于2的结点)。 子树有左右之分,其次序不能颠倒。 二叉树可以是空集合,根可以有空的左子树或空的右子树。 树和二叉树区别 二叉树不是树的特殊情况,它们是两个概念 二叉树结点的子树要区分左子树和右子树,即使只有一棵子树也经行区分,说明它是左子树,还是右子树。 树当结点只有一个孩子时,就无须区分它是左还是右的次序。因此二者是不同的。这是二叉树与树的最主要的差别。 (也就是二叉树每个结点位置或者次序都是固定的,可以是空,但是个可以说它没有位置,而树的结点位置是相对于别的结点来说的,没有别的结点时.它就无所谓左右了)
3.案例引用
4.树和二叉树的抽象数据类型定义
5.二叉树的性质和存储结构 1.性质 满二叉树与完全二叉树 满二叉树 :一颗深度为 且有 个结点的二叉树就叫满二叉树
完全二叉树 :深度为 的具有 个结点的二叉树,当且仅当每一个结点都与深度为 的满二叉树 中的编号 为 的结点一一对应时,称为完全二叉树 (叶子结点的编号是连续的,左侧树必须满元素 )
叶子只可能分布在层次最大的两层上, 对任意结点,如果其右子树的最大层次为i,则其左子树的最大层次必为i或i+1
性质1 在二叉树的第i层上至多有 个结点
性质2 深度为k的二叉树至多有 个结点(k>=1)
性质3 对任何一个二叉树 ,如果其叶子为 ,度为2的结点为 ,\则
eg:这里叶子结点7 8 9 10 11 12有6个,度为2的有1 2 3 4 5 有五个
总结点数n 又
总边个数B =
性质4 具有n个结点的完全二叉树 的深度为
性质5 如果有一颗 个结点的完全二叉树( 深度为 [ )的结点按层编号(从第1层到第[ 层,每层从左到右),对任一结点i (1<= <=n),有
如果 =1,则结点i是二叉树的根,无双亲;如果i>1,则其双亲是结点[i /2]。 如果2 ,则结点i为叶子结点,无左孩子;否则, 其左孩子是结点2i。 如果 ,则结点 无右孩子;否则,其右孩 子是结点2i + 1。 总结 : 编号为i,他的父结点为 ,左结点为2i,右节点
2.存储结构
1.二叉树的顺序存储结构 2.二叉树的链式存储结构 typedef struct BiNode { int data; struct BiNode *lchild ,*rchile ; }BiNode,*BiTree;
在n个结点的二叉链表中,有 个空指针域
必有2n个链域,除根节点外,每个结点有且仅有一个双亲,所有只会有n-1个结点的链域存放指针,指向非空子女结点
3.三叉链表 typedef struct TriTNode { int data; struct TriTNode *Ichild ,*parent ,*rchild ; }TriTNode,*TriTree;
3.遍历二叉树和线索二叉树 1.类型 先序遍历:根左右
中序遍历:左根右
后续遍历:左右根
先—根左右
ABELDHMIJ
中—左根右
ELBAMHIDJ
后—左右根
LEBMIHJDA
实例:
2.根据遍历序列确定二叉树(先 中 后序) 实例1:先序+中序 先:A B C D E F G H I
中:C D B F E A I H G J
解题思路
由先知A必为根,B必为左 由中知 CDBFE在左部,IHGJ在右边
由先序知道B为根,由中序知道CD为左子树,FE为右子树
由先序知道G是根,那么I H为左子树,J为右子树
由中序CD左根右知道,c为左,d为右,先序知道E为根,中序知道F为左
由中序知道I为左子树
实例2—中序+后续 中序序列:BDCEAFHG 后序序列:DECBHGFA 由中序后续知道根为A,BDCE为左根 FHG为右根 后序知道B为根,中序推出没有左根,c为下一个根 左右根D为左,E为右边 后序知道F为根 由中序知道F没有左,那么H为左,G为根
3.遍历的算法实现-先序遍历
代码实现 void PerOrderTraverse (BiTree T) { if (T!=NULL ){ printf ("%d\t" ,T->data); PerOrderTraverse(T->lchild); PerOrderTraverse(T->rchile); } }
递归代码解释 首先进入函数,此时T为传入的根结点
打印根节点
第一次调用:根左右 函数指向根B
进入第二层循环 遍历左,左为空此时返回
回到第二层循环此时 PerOrderTraverse(T->lchild);为空,那么自动执行下一条语句PerOrderTraverse(T->rchile);
进入循环,执行到D
再向下执行为空返回到第一次循环
再执行C
(●ˇ∀ˇ●)明白了吗!
4.遍历的算法实现-中序遍历 void PerOrderTraverse (BiTree T) { if (T!=NULL ){ PerOrderTraverse(T->lchild); printf ("%d\t" ,T->data); PerOrderTraverse(T->rchile); } }
5.历的算法实现-后序遍历 void PerOrderTraverse (BiTree T) { if (T!=NULL ){ PerOrderTraverse(T->lchild); PerOrderTraverse(T->rchile); printf ("%d\t" ,T->data); } }
6.二叉树遍历小总结
时间复杂度O(n)//每个结点只访问一次
空间复杂度O(n)//栈占用的最大辅助空间
7.中序遍历非递归算法-栈 void InOrderTraverse (BiTree T) { BiTree P; InitStack(S); P=T; while (p||StackEmpty(S)) { if (P) { Push(S,p); p=p->Ichild; } else { Pop(S,q); printf ("%d" ,q->data); p=q->rchild; } } }
8.二叉树的层次遍历
typedef struct { BTNode data[MAXSIZE]; int front,rar; }SqQueue; void LevelOrder (BTNode *b) { BTNode *p; SqQueue *qu; initQueue(qu); enQuenue(qu,b); while (!QueueEmpty(qu)){ deQueue(qu,p); printf ("%c" ,p->data);访问结点p if (p->Ichild!=NULL ) enQueue (qu,p->Ichild) ; if (p->rchild!=NULL )enqueue(qu,p->rchild); } }
9.二叉树遍历算法的应用 1.二叉树的建立 void CreateBiTree (BiTree &T) { char ch; scanf (&ch);if (ch=="#" ) T=NULL ;else { if (!(T=(BiTNode *)malloc (sizeof (BiTNode)))) exit (OVRTFLOW); T->data=ch; CreateBiTree(T->Ichild); CreateBiTree(T->rchild); } }
2.复制二叉树 int Copy (BiTree T,BiTree &newT) { if (T==NULL ) { NewT=NULL ; return 0 ; }else { NewT=new BiTNode; NewT->data=T->data; Copy(T->lchild,NewT->lchild); Copy(T->rchild,NewT->rchild); } return OK; }
3.计算二叉树的深度 4.计算二叉树的结点总数 如果为空树则结点为0 否则,结点个数为左子树个数+右子树结点个数再+1 int NodeCount (Bitree T) { if (T==NULL ) return 0 ; else return NodeCount(T->lchild)+ NodeCount(T->rchild)+1 ; }
5.计算叶子结点的个数 如果是空树返回0 否则,为左子树的叶子结点+右子树的叶子结点 int LeafCount (BiTree T) { if (T==NULL ) return 0 ; if (T->lchild==NULL &&T->rchile==NULL ) return 1 ; else return LeafCount(T->lchild)+leafCount(T->rchile); }
10.线索二叉树 如果某个结点的左孩子为空,则将空的左孩子指针域改为指向其前驱 ; 如果某孩子的右结点为空,则将空的右孩子的指针域改为指向其后继 ;
这里是依照遍历来判断前驱后继,而不是图
1.定义 typedef struct BiThNode { int data; int ltag,rtag; struct BiThNode *lchild ,*rchild ; };
2.线索二叉树画法
3.遍历算法 408不要求掌握
11.李阳的交换左右子树 #include <stdio.h> #include <stdlib.h> typedef struct TreeNode { int val; struct TreeNode *left ; struct TreeNode *right ; }TreeNode; TreeNode * creatNode (int val) { TreeNode * newNode=(TreeNode*) malloc (sizeof (TreeNode)); if (!newNode) { printf ("失败的树 " ); exit (-1 ); } newNode->val=val; newNode->left=NULL ; newNode->right=NULL ; return newNode; } void swaplr (TreeNode * root) { if (root==NULL ) { return ; } TreeNode * temp=root->left; root->left=root->right; root->right=temp; swaplr(root->left); swaplr(root->right); } void preOrderTraversal (TreeNode*root) { if (root==NULL ) { return ; } printf ("%d " , root->val); preOrderTraversal(root->left); preOrderTraversal(root->right); } void freeTree (TreeNode* root) { if (root == NULL ) { return ; } freeTree(root->left); freeTree(root->right); free (root); } int main () { TreeNode * root=NULL ; root= creatNode(1 ); root->left = creatNode(2 ); root->right = creatNode(3 ); root->left->left = creatNode(4 ); root->left->right = creatNode(5 ); preOrderTraversal(root); printf ("\n" ); swaplr(root); printf ("Pre-order traversal after swapping:\n" ); preOrderTraversal(root); printf ("\n" ); freeTree(root); return 0 ; }
6.树和森林 1.定义 森林 :是 棵互不相交的树的集合
2.双亲表示法 实现 :定义结构数组
存放树的结点
每个结点含两个域
数据域 :存放结点本身信息
双亲域 :指示本结点的双亲结点在数组中的位置
特点 :找双亲容易,找孩子难
typedef struct PTNode { TElemType data; int parent; }PTNode; typedef struct { PTNode nodes[MAXTSIZE]; int n,r; }PTree;
3.孩子链表 把每个结点的孩子结点排列起来,看成是一个线性表, 用单链表存储,则n个结点有n个孩子链表(叶子的孩子链表为空表)。而n个头指针又组成一个线性表,用顺序表(含n个元素的结构数组)存储。
解释 :每个结点都有一个单链表,叶子节点的单链表是空表,然后再将这些链表的头指针存放在数组中
孩子结点结构
typedef struct CTNode { int child; struct CTNode *next ; }* ChildPrt;
双亲结点结构
typedef struct { TElemType data; ChildPrt firstchild; }CTBox;
树结构
typedef struct { CTBox nodes[MAXTSIZE]; int n,r; }CTree;
特点 :找孩子容易,找双亲难
4.*孩子兄弟表示法(二叉树表示法,二叉链表表示法) 1.定义 实现 :用二叉链表作树的存储结构,链表中的美观结点的指针域分别指向其第一个孩子节点 和下一个兄弟节点
typedef struct CSNode { ELemtype data; struct CSNode *firstchild ,*nextsibling ; }CSNode,*CSTree;
左孩子 右兄弟,是兄弟的就来砍我
特点:找孩子,找兄弟简单,找双亲难
5.*树与二叉树的转换 1.定义 将树转化为二叉树,利用二叉树的算法实现对树的操作 由于树和二叉树都可以用二叉链表作存储结构,则以二叉树链表作媒介可以导出树与二叉树之间的对应关系
2.操作 1.将树转为二叉树 加线:在兄弟之间加一连线
抹线:对每个结点,除了其左孩子外,去除其与其余孩子之间的关系
旋转:以树的根结点为轴心,将整树顺时针转45°
2.将二叉树转为树 加线:若p结点是双亲结点的左孩子,则将p的右孩子,右孩子的右孩….沿分支找到的所有右孩子,都与p的双亲用线连起来 抹线:抹掉原二叉树中双亲与右孩子之间的连线 调整:将结点按层次排列,形成树结构
6.*森林和二叉树的转换(二叉树与多棵树之间的关系) 1.森林转化为二叉树 将各棵树分别转换成二叉树
将每棵树的根结点用线相连
以第一棵树根结点为二叉树的根,再以根结点为轴心,顺时针旋转,构成二叉树型结构
2.二叉树转为森林 抹线:将二叉树中根结点与其右孩子连线,及沿右分支搜索到的所有右孩子间连线全部抹掉,使之变成孤立的二又树(去掉全部右孩线)
还原:将孤立的二又树还原成树(孤立二叉再还原)
7.树与森林的遍历 1.树的遍历的三种方式{先根(次序),后根,层次遍历}
2.森林的遍历 将森林看作3部分构成
森林中第一棵树的根结点; 森林中第一棵树的子树森林, 森林中其它树构成的森林。 1.先序遍历 若森林不空 则:
访问森林中第一棵树的根结点; 先序遍历 森林中第一棵树的子树森林;先序遍历 森林中(除第一棵树之外)其余树构成的森林。
2.中序遍历 若森林不空,则
1.中序遍历 森林中第一棵树的子树森林;
2.访问森林中第一棵树的根结点;
3.中序遍历 森林中(除第一棵树之外)其余树构成的森林。
对应:213
3.小案例
7.*哈夫曼树及其应用 408要求: 1.哈夫曼(Huffman)树和哈夫曼编码 2.并查集及其应用 3.堆及其应用(25新增)
1.基本概念 路径 :从树中的一个结点到另一个结点之间的分支构成这两个结点间的路径
结点路径的长度 :两结点间的路径上的分支数
树的路径长度 :从树根到每一个结点的路径长度之和,记作TL
结点数目相同的二叉树中,完全二叉树是路径长度最短的二叉树
权 :将树中结点赋给一个有着某种含义的数值,则这个数值称为该结点的权
结点的带权路径长度 :从根节点 到该结点之间的路径长度 与该结点的权 的乘积
树的带权的路径长度 :树中的所有叶子 结点带权路径长度之和 (WPL)
最优树 :带权路径最短的树(最优树)度要相同
最优二叉树 :带权路径长度(WPL)最短的二叉树
满二叉树不一定是哈夫曼树
具有相同带权结点的哈夫曼树不唯一
贪心算法 :构造哈夫曼树时首先选择权最小的叶子结点
2.哈夫曼算法 1.构造方法 根据 个给定的权值 构成的 棵二叉树的森林 ,其中 只有一个带权为 的根节点
构造森林全是根
在 中选取两棵根结点的权值最小的树作为左右子树,构造一棵新的二叉树,且设置新的二叉树的根结点的权值为其左右子树上根结点的权值之和。
选用两小造新树
在 中删除这两棵树,同时将新得到的二叉校加入森林中。·
删除两小添新人
4. 重复2,3剩单根
总结 哈夫曼树的结点只有度为0或2的没有度为1的结点 包含n各叶子结点的哈夫曼树共有 个结点 包含n棵树的森林要经过n-1次合并才能形成哈夫曼树,共产生n-1个新结点 在哈夫曼算法中,初始时有n棵二叉树,要经过n-1次合并最终形成哈夫曼树。 经过n-1次合并产生n-1个新结点,且这n-1个新结点都是具有两个孩子的分支结点。 可见:哈夫曼树中共有n+n-1 = 2n-1个结点,且其所有的分支结点的度均不为1。 2.哈夫曼树的算法 顺序结构(一维数组) typedef struct { int weight; int parent,lch,rch; }HTNode,*HUffmanTree;
void CreatHuffmanTree (HuffmanTree &HT, int n) { int m,i; if (n<=1 )return ; m=2 *n-1 ; HT=new HTNode[m+1 ]; for (i=1 ;i<=m;++i){ HT[i].lch=0 ; HT[i].rch=0 ; HT[i].parent=0 ; } for (i=1 ;i<=n;++i) cin >>HT[i].weight;for ( i=n+1 ;i<=m; i++){ Select(HT, i-1 ,s1,s2); HT[s1].parent=i; HT[s2] .parent=i; HT[i].llch=s1;HT[i].rch=s2; HT[i].weight=HT[s1].weight + HT[s2].weight; } }
初始化HT[1…….2n-1]: lch=rch=parent=0 输入初始n个叶子结点:置HT[1…..n]的weight值; 进行以下n-1次合并,依次产生n-1个结点HT[i], i=n+1…..2n-1: a)在HT[1..i-1]中选两个未被选过(从parent ==0的结点中选)的weight最小的两个结点HT[s1]和HT[s2], s1、s2为两个最小结点下标; b)修改HT[s1]和HT[s2]的parent的值:HT[s1].parent=i;HT[s2].parent=i; c)修改新产生的HT[i]: .HT[i].weight=HT[s1].weight + HT[s2].weight;. HT[i]. Ich=s1; HT[i]. rch=s2; 3.哈夫曼编码 前缀编码:任意一字符不是另一个字符的前缀
1.方法 1、统计字符集中每个字符在电文中出现的平均概率 (概率越大, 要求编码越短)。 2、利用哈夫曼树的特点:权越大的叶子离根越近;将每个字符的概率值作为权值,构造哈夫曼树。则概率越大的结点,路径越短。 3、在哈夫曼树的每个分支上标上O或1: 结点的左分支标0,右分支标1 把从根到每个吐子的路径上的标号连接起来,作为该叶子代表的字符的编码。
左分支标记0,右分支标记1
问题 1.为什么哈夫曼编码能够保证是前缀编码?
因为没有一片树叶是另一片树叶的祖先,所以每个叶结点的编码就不可能是其它叶结点编码的前缀
2.为什么哈夫曼编码能够保证字符编码总长最短? 因为哈夫曼树的带权路径长度最短,故字符编码的总长最短。
性质 1:哈夫曼编码是前缀编码
性质 2:哈夫曼编码是最优前缀码
2.实现 void CreatHuffmanCode (HuffmanTree HT, HuffmanCode & HC, int n) { int i,cd,start; HC=new char *[n+1 ]; cd=new char [n]; cd[n-1 ]='\0’;//编码结束符 for(i=1; i<=n; ++i){//逐个字符求哈夫曼编码 start=n-1; c=i; f=HT[i].parent; while(f!=O){//从叶子结点开始向上回溯,直到根结点 --start;//回溯一次start向前指一个位置 if (HT[f].Ichild= =c) cd[start]= ' O' ;//结点c是f的左孩子,则生成代码O else cd[start]= ' 1 ' ;//结点c是f的右孩子,则生成代码1 c=f; f=HT[f].parent;//继续向上回溯 } //求出第i个字符的编码 HC[i]= new char [n-start];//为第i个字符串编码分配空间 strcpy(HC[i],&cd[start]);//将求得的编码从临时空间cd复制到HC的当前行中 } delete cd;//释放临时空间 }// CreatHuffanCode
4.编码的实现
八、图 1.图的定义和基本术语 1.图的定义 图 :
V:顶点(数据元素)的有穷非空集合
E:边的有穷集合
无向图 :每条边都没有方向
有向图 :每条边都有方向
完全图 :任意两个点都有一条边相连
稀疏图 :有很少边或弧的图(e<nlogn)
稠密图 :有较多边或弧的图
网 :边/弧带权的图
邻接 :有边/弧相连的两个顶点之间的关系。
存在 ,则称 和 ;互为邻接点 ;(无向图)
存在$$,则称v邻接到 ,邻接于 (有向图)
关联 :边/弧与顶点的关系
存在$(V_i,V_j)/称 为 该 边 弧 关 联 于 v_i和 v_j$
顶点的度 :与该顶点相关联的边的数目,记为TD(v)
在有向图中,顶点的度等于该顶点的入度与出度之和。
顶点v的入度是以v为终点的有向边的条数,记作ID(v)
顶点v的出度是以v为始点的有向边的条数,记作OD(v)
路径 :接续的边构成的顶点序列
路径长度 :路径上边或弧的数目/权值之和。回路(环) :第一个顶点和最后一个顶点相同的路径。简单路径 :除路径起点和终点可以相同外,其余顶点均不相同的路径。
简单回路(简单环) :除路径起点和终点相同外,其余顶点均不相同的路径。
连通图(强连通图) 在无(有)向图 中,若对任何两个顶点v、u都右在从v至到u的路径称G是连通图(裾连通图)
权与网 图中边或弧所具有的相关数称为权 。表明从一个顶点到另一个顶点的距离或耗费。 带权的图称为网 。
子图 设有两个图G= (V,{E})、G1= (V1,{E1}),若V1⊆V,E1⊆E,则称G1是G的子图
连通分量(强连通分量) 无向图G的极大连通子图 称为G的连通分量 。 极大连通子图意思是:该子图是G连通子图,将G的任何不在该子图中的顶点加入,子图不再连通。
有向图G的极大强连通子图称为G的强连通分量
极大强连通子图意思是:该子图是G的强连通子图,将D的任何不在该子图中的顶点加入,子图不再是强连通的。
极小连通子图: 该子图是G的连通子图,在该子图中删除任何一天边子图不再连通。生成树: 包含无向图G所有顶点的极小连通子图。生成森林: 对非连通图,由各个连通分量的生成树的集合。
2.图的类型定义 1.图的抽象数据类型定义如下: ADT Graph{数据对象V :具有相同特性的数据元素的集合,称为顶点集。 数据关系R: R={VR} VR={|| v,w⊆V ^ p(v,w),表示从v到w的弧,P(v,w)定义了弧的信息
}
2. 图的操作
3.图的存储结构
1.数组(邻接矩阵)表示法
无向图邻接矩阵
分析1:无向图的邻接矩阵是对称的
分析2:顶点i的度=第i行(列)中的1的个数
特别:完全的邻接矩阵中,对角元素为0,其余为1
有向图的邻接矩阵
分析1:有向图的邻接矩阵可能不是对称的
分析2:顶点的出度=第i行元素之和( 指向&V_2& 和 )
顶点的出度=第i列元素之和
顶点的度=第i行元素之和+第i列元素之和
有向网的邻接矩阵 网(即有权图)的邻接矩阵表示法
定义为
V_i V_j或 V_i,V_j∊
无 边 弧
如果两个顶点之间存在弧或边,那么我就记录两个顶点为权 ,如果不存在则记录无穷大
2.邻接矩阵的存储形式 1.用两个数组分别存储顶点表和邻接矩阵 #define MVNum 100 typedef char VerTexType;typedef int ArcType;typedef struct {VerTexType vexs[MVNum]; ArcType arcs[MVNum][MVNum]; int vexnum, arcnum;}AMGraph;
2.采用邻接矩阵表示法创建无向网 算法思想 (1)输入总顶点数和总边数。
(2)依次输点的信息存人顶点表中。
(3)初始化邻接矩阵,使每个权值初始化为极大值。
(4)构造邻接矩阵
代码先欠着
3.邻接矩阵的好处和坏处 好处 直观、简单、好理解 方便检查任意一对顶点间是否存在边 方便找任一顶点的所有“邻接点”(有边直接相连的顶点) 方便计算任一顶点的“度”(从该点发出的边数为“出度”,指向该点的边数为“入度”)·无向图:对应行(或列)非O元素的个数·有向图:对应行非0元素的个数是“出度”;对应列非0元素的个数是“入度” 坏处 3.邻接表表示法(链式) 1.无向图的邻接表 ·按编号顺序将顶点数据存储在一维数组中;·
关联同一顶点的边(以顶点为尾的弧)︰
·用线性链表存储
data表示顶点本身,firstarc表示第一条边的指针(以v1为例子,下标为3或为1的元素的指针)adjvex表示邻接的顶点,nextarc表示下一元素的指针
特点: ·邻接表不唯一 ·若无向图中有 个顶点、 条边,则其邻接表需 个头结点和 个表结点。适宜存储稀疏图。 无向图中顶点v的度为第i个单链表中的结点数。 2.有向图
特点: 顶点 的出度 为第i个单链表中的结点个数。 顶点 的入度 为整个单链表中邻接点域值是 的结点个数。
3.链式代码 1.定义代码 顶点
typedef struct VNode { VerTexType data; ArcNode * firstarc; }VNode,AdjList[MVNum];
边结点
#define MVNum 100 typedef struct ArcNode {int adjvex;struct ArcNode * nextarc ;OtherInfo info; }ArcNode;
图结点
typedef struct { AdjList vertices; int vexnum, arcnum; } ALGraph;
2.采用邻接表表示法创建无向网的算法思想 【算法思想】
输入总顶点数和总边数。 建立顶点表 依次输入点的信息存入顶点表中 使每个表头结点的指针域初始化为NULL 创建邻接表 依次输入每条边依附的两个顶点确定两个顶点的序号i和j,建立边结点 将此边结点分别插入到 和 对应的两个边链表的头部 5.邻接表的特点 1.特点 4.邻接矩阵与邻接表表示方法的关系
2.联系: 邻接表中每个链表对应于邻接矩阵中的一行,链表中结点个数等于一行中非零元素的个数。
2.区别: 对于任一确定的无向图,邻接矩阵是唯一的(行列号与顶点编号一致),但邻接表不唯一(链接次序与顶点编号无关)。
邻接矩阵的空间复杂度为 ,而邻接表的空间复杂度为
3.用途: 邻接矩阵多用于榈密图;而邻接表多用于稀疏图
5.十字链表
1.简介 十字链表 (Orthogonal List)是有向图 的另一种链式存储结构。我们也可以把它看成是将有向图的邻接表和逆邻接表结合起来形成的一种链表。 有向图中的每一条弧对应十字链表中的一个弧结点 ,同时有向图中的每个顶点在十字链表中对应有一个结点,叫做顶点结点。
2.具体 data:数据
firstin:第一个入度边
firstout:第一个出度边
tailvex:弧尾位置
headvex:弧头位置
hlink:弧头相同的下一条弧
tlink:弧尾相同的下一条弧
6.邻接多重表
4.图的遍历 1.定义 遍历定义:
从已给的连通图中某一顶点出发,沿着一些边访遍图中所有的顶点,且使每个顶点仅被访问一次,就叫做图的遍历 ,它是图的基本运算
遍历实质 :找每个顶点的邻接点的过程。
图的特点 : 图中可能存在回路 ,且图的任一顶点都可能与其它顶点相通在访问完某个顶点之后可能会沿着某些边又回到了曾经访问过的顶点。
如何避免回路 :
解决思路:设置辅助数组 ,用来标记每个被访问过的顶点。
初始状态 为0 ·顶点i被访问,改 为1,防止被多次访问 2.深度优先(DFS) 1.连通图的遍历 方法: 在访问图中某一起始顶点v后,由v出发,访问它的任一邻接顶点w1 再从w出发,访问与w邻接但还未被访问过的顶点W2; 然后再从w出发,进行类似的访问,… 如此进行下去,直至到达所有的邻接顶点都被访问过的顶点u为止。接着,退回一步,退到前一次刚访问过的顶点,看是否还有其它没有被访问的邻接顶点。 如果有,则访问此顶点,之后再从此顶点出发,进行与前述类似的访问; 如果没有,就再退回一步进行搜索。重复上述过程,直到连通图中所有顶点都被访问过为止。
实现
bool visited[MAX_VERTEX_NUM];void DFSTraverse (Graph G) { int v; for (v=0 ;v<G;v++) { visited[v]=false ; for ( v = 0 ; v < G; ++v) { if (!visited[v]) { DFS(G,v); } } } } void DFS (Grap G,int V) { int w; visit(v); visited[v]=true ; for (w=FirstNeighbor(G,v);w>=0 ;w=NextNeighbor(G,v,w)) { if (!visited[w]) { DFS(G,w); } } }
效率分析 用邻接矩阵来表示图,遍历图中每一个顶点都要从头扫描该顶点所在的行,时间复杂度为 。
用邻接表来表示图,虽然有 个表结点,但只需扫描e个结点即可完成遍历,加上访问n个头结点的时间,时间复杂度为 .
结论 :稠密图 适于在邻接矩阵上进行深度遍历;稀疏图 通于在邻接表上进行深度遍历。2.广度优先遍历 1.方法 方法:从图的某一结点出发,首先依次访问该结点的所有邻接点 ,再按这些顶点被访问的先后次序依次访问与它们相邻接的所有未被访问的顶点 重复此过程,直至所有顶点均被访问为止。
2.实现 bool visited [Max_VERTEX_NUM];void BFSTraverse (Grap G) { for (int i = 0 ; i < G.vexnum; ++i) { visited[i]=false ; } InitQueue(Q); for ( i = 0 ; i <G.vexnum ; ++i) { if (!visited[i]){ BFS(G,i); } } } void BFS (Graph G,int v) { visit(v); visited[v]=true ; Enqueue(Q,v); while (!isEmpty(Q)){ DeQueue(Q,v); for (int w = FirstNeighbor(G,v); w>0 ; w=NextNeighbor(G,v,w)) { if (!visited[w]){ visit(w); visited[w]=true ; Enqueue(Q,w); } } } }
3.效率分析 如果使用邻接矩阵,则BFS对于每一个被访问到的顶点,都要 循环检测矩阵中的整整一行( n个元素),总的时间代价为O( )。 用邻接表来表示图,虽然有2e个表结点,但只需扫描e个结点即可完成遍历,加上访问n个头结点的时间,时间复杂度为 。 4.效率比较 ·空间复杂度相同,都是O(n)(借用了堆栈或队列) ; ·时间复杂度只与存储结构,(邻接矩阵或邻接表)有关,而与搜索路径无关。 5.图的应用 1.最小生成树 1.生成树的简介 生成树 :所有顶点均由边连接在一起,但不存在回路
一个图可以有许多棵不同的生成树
所有生成树具有以下共同特点
生成树的顶点个数与图的顶点个数相同; 生成树是图的极小连通子图 ,去掉一条边则非连通;· 一个有n个顶点的连通图的生成树有 条边; ·在生成树中再加一条边必然形成回路。 生成树中任意两个顶点间的路径是唯一 的; 含有 个顶点 条边的图不一定是最小生成树
2.无向图的生成树
3.最小生成树 最小生成树 :给定一个无向网络在该网的所有生成树中,使得各边权值之和最小的那棵生成树称为该网的最小生成树,也叫最小代价生成树。
构造最小生成树 构造最小生成树的算法很多,其中多数算法都利用了MST 的性质。
MST性质 :设N =(V, E)是一个连通网,U是顶点集V的一个非空子集。若边(u, v)是一条具有最小权值的边,其中u∈u,v∈V-U,则必存 在一棵包含边(u, v)的最小生成树。
Prim-普里姆算法 算法思想
设 是连通网, 是N上最小生成树中边的集合。
初 始 令 ,∊ TE=${ }。
在所有∊ ,∊ 的边∊ 中,找一条代价最小的边 。
将 并入集合 ,同时 并入U
重复上述操作直至 ,则 为 的最小生成树。
从顶点往下找最小的权
Kruskal-克鲁斯卡尔算法 所有边按权值排序,然后选择最小的
当有循环时舍弃这条边
当所有边连通时结束
与prim算法不同的是他是按排序来找最小,prim是依次选最小
设连通网 ,令最小生D树初始状态为只有n个顶点而无边的非连通图 ,每个顶点自成一个连通分量。
在E中选取代价最小的边,若该边依附的顶点落在T中不同的连通分量上(即:不能形成环),则将此边加入到T中;否则,舍去此边,选取下一条代价最小的边。
依此类推,直至T中所有顶点都在同一连通分量上为止。
最小生成树可能不唯一
两种比较
2.最短路径 1.定义 最短路径与最小生成树不同 ,路径上不一定包含n个顶点,也不一定包含n-1条边。
单源最短路径-Dijkstra迪杰斯特拉算法
所有顶点间的最短路径—Floyd弗洛伊德算法
2.Dijkstra算法 概述 1.初始化:先找出从源点 到各终点v的直达路径 , 即通过一条弧到达的路径。
2选择:从这些路径中找出一条长度最短的路径 。
3.更新:然后对其余各条路径进行适当调整:
若在图中存在弧 ,且 则以路径 代替 。
在调整后的各条路径中,再找长度最短的路径)依此类推。
(先找出直达的,然后与不直达的比较,有小的就更新被比较的)
具体-按路径长度递增次序产生最短路径 1、把V分成两组:辅助数组D存放。 (1) S:已求出最短路径的顶点的集合。 (2)T=V -s∶尚未确定最短路径的顶点集合。
2、将T中顶点按最短路径递增的次序加入到S中,
保证:(1)从源点 到S中各顶点的最短路径长度都不大于从 到T中任何顶点的最短路径长度。
(2)每个顶点对应一个距离值: S中顶点:从 到此顶点的最短路径长度。
T中顶点:从 到此顶点的只包括S中顶点作中间顶点的最短路径长度。
3.Floyd 弗洛伊德算法 算法思想 ·逐个顶点试探 ·从到v,的所有可能存在的路径中· 选出一条长度最短的路径
3.拓扑排序 有向无环图:无环的有向图,简称DAG
一个结点可能有多个前驱,但是没有回路
1.AOV网 定义 用一个有向图表示一个工程的各子工程及其相冥制约的关系,其中以顶点表示活动 ,弧表示活动之间的优先制约关系 ,称这种有向图为顶点表示活动的网 ,简称AOV网(Activity On Vertex network)
特点 若从i到j有一条有向路径,则i是j的前驱;j是i的后继。 若是网中有向边,则i是j的直接前驱;j是i的直接后继 AOV网中不允许有回路,因为如果有回路存在,则表明某项活动以自己为先决条件,显然这是荒谬的。 检测AOV 网中是否存在环方法: 对有向图构造其顶点的拓扑有序序列,若网中所有顶点都在它的拓扑有序序列中,则该AOV 网必定不存在环。
2.AOE网 用一个有向图表承一个工程的各子工程及其相互制约的关系,以弧表示活动 ,以顶点表示活动的开始或结束事件 ,称这种有向图为边表示活动的网 ,简称为AOE网(Activity On Edge)。
3.拓扑排序 在AOV网没有回路的前提下,我们将全部活动排列成一个线性序列,使得若AOV网中有弧存在,则在这个序列甲,I一疋排仕J的前面,具有这种性质的线性序列称为拓扑有序序列 ,相应的拓扑有序排序的算法称为拓扑排序。
4.关键路径
对于AOE网,我们关心两个问题:
(1)完成整项工程至少需要多少时间?
(2)哪些活动是影响工程进度的关键?
关键路径 :路径长度最长的路径。 路径长度:路径上各活动持续时间之和。
———>求解关键路径问题
由若干个关键活动组成的就是关键路径
ve(j)最早开始时间:从原点开始找到权的最大值
vl(n)最迟发生时间:从汇点向前,找到最小值
具体计算
e(i)就是弧尾的长度
eg: v5就是6(v5连接的是v2,只用算v2),
l(i)就是vl-路径上的权值
eg: v5就是7-a4的权=7-1=6
讨论 1、若网中有几条关键路径,则需加快同时在几条关键路径上的关键活动。 如: a11、a10、a8、a7。
2、如果一个活动处于所有的关键路径上,那么提高这个活动的速度,就能缩短整个工程的完成时间。如: a1、a4。
3、处于所有的关键路径上的活动完成时间不能缩短太多),否则会使原来的关键路径变成不是关键路径。这时,必须重新寻找关键路径。
如:a1由6天变成3天,就会改变关键路径。