数组
1.数组的名字也是一个指针,该指针指向数组的第一个元素,但是C++中并没有记录数组的大小,因此访问时需要确定没有超出数组的边界,例如,a[3]超出了边界
int a[] = {1,2,3}; //a为指针
2.数组的定义
//普通一维数组
int arr[10];
int a[] = {1,2,3};
//普通二维数组
int arr[2][2] =
{
{1,1},
{1,1}
};
//一维动态数组
#include<iostream>
using namespace std;
int main()
{
int n;
cin>>n;
//分配动态一维数组
int *arr=new int[n];
for(int i=0;i<n;i++)
cin>>arr[i];
for(int i=0;i<n;i++)
cout<<arr[i]<<" ";
//释放arr数组
delete[] arr;
return 0;
}
//二维动态数组
#include<iostream>
using namespace std;
int main()
{
int row,col;
cin>>row>>col;
//为行指针分配空间
int **arr=new int *[row];
for(int i=0;i<row;i++)
arr[i]= new int[col];//为每行分配空间(每行中有col个元素)
//输入二维数组的数
for(int i=0;i<row;i++)
for(int j=0;j<col;j++)
cin>>arr[i][j];
cout<<"*******************"<<endl;
//输出二维数组中的数
for(int i=0;i<row;i++)
{
for(int j=0;j<col;j++)
cout<<arr[i][j]<<" ";
cout<<endl;
}
//释放二维数组(反过来)
for(int i=0;i<row;i++)
delete[] arr[i];
delete[] arr;
return 0;
}
3.在C/C++中,当数组作为函数的参数进行传递时,数组就自动退化为同类型的指针,详见《剑指offer》38页
int GetSize(int data[])
{
return sizeof(data);
}
int main()
{
int data1[] = {1,2,3};
int size1 = sizeof(data1); //size1=12
int* data2 = data1;
int size2 = sizeof(data2); //size2=4
int size3 = Getsize(data1); //size3=4
return 0;
}
4.vector是一种动态数组,其详细使用介绍详见标准模板库STL_Haocan Xu
链表
链表与数组
1. 区别
1.1 数组静态分配内存,链表动态分配内存。
1.2 数组在内存中是连续的,链表是不连续的。
1.3 数组利用下标定位,查找的时间复杂度是O(1),链表通过遍历定位元素,查找的时间复杂度是O(N)。
1.4 数组插入和删除需要移动其他元素,时间复杂度是O(N),链表的插入或删除不需要移动其他元素,时间复杂度是O(1)。
2. 数组的优缺点
2.1 优点1——随机访问性比较强,可以通过下标进行快速定位。
2.2 优点2——查找速度快
2.3 缺点1——插入和删除的效率低,需要移动其他元素。
2.4 缺点2——会造成内存的浪费,因为内存是连续的,所以在申请数组的时候就必须规定七内存的大小,如果不合适,就会造成内存的浪费。
2.5 缺点3——内存空间要求高,创建一个数组,必须要有足够的连续内存空间。
2.6 缺点4——数组的大小是固定的,在创建数组的时候就已经规定好,不能动态拓展。
3. 链表的优缺点
3.1 优点1——插入和删除的效率高,只需要改变指针的指向就可以进行插入和删除。
3.2 优点2——内存利用率高,不会浪费内存,可以使用内存中细小的不连续的空间,只有在需要的时候才去创建空间。大小不固定,拓展很灵活。
3.3 缺点1——查找的效率低,因为链表是从第一个节点向后遍历查找。
- 链表的定义
//链表的定义
struct node{
int data; //数据域
node *next; //指针域,只想下一个元素
};
- 创建新链表
//链表的创建
node* creatlist(int arr[],int len){ //建立链表
node*head,*pre,*p;
head=new node; //创造头结点
head->next=NULL;
pre=head; //pre赋值
for(int i=0;i<len;i++){
p=new node;
p->data=arr[i]; //赋值给数据域
p->next=NULL; //最后一个节点的指针域置空
pre->next=p; //新结点连到链表的结尾
pre=p; //pre指向链表的最后一个结点
}
return head;
}
- 插入
//链表的插入
void insert(node*head,int pos,int x){ //实现将一个数据插入到链表的指定位置pos处
node*p,*pre;
pre=head;
p=new node; //分配新结点
p->data=x;
while(--pos){ //将pre指针定位到pos位置的前面
pre=pre->next;
}
p->next=pre->next; //先将pre的next赋值给p ,将pre所指结点后面所有的结点连接到p所指的结点
pre->next=p; //将p所指的结点链接到pre所指结点后面。
}
- 查找
//链表的查找
int search(node*head,int x){ //实现在链表中查找数据域等于x的结点的个数
int count=0; //计数器,初始化为零
node*p=head->next;
while(p!=NULL){
if(p->data==x){ //找到一个结点,计数器加一
count++;
}
p=p->next; //p往后挪一个位置
}
return count; //返回查找结果
}
- 删除
//链表的删除
void del(node*head,int x){ //实现将链表所有数据域等于x的结点删除
node *pre,*p;
pre=head; //pre指针始终指向被删除结点的前置结点
p=pre->next; //p指针为工作指针,用于遍历链表
while(p!=NULL){
if(p->data ==x){ //如果是要被删除的结点
pre->next =p->next;
delete(p); //要记得用过的内存还给操作系统
p=pre->next ; //p指针更新到pre指针的后面
}
else {
pre=p; //如果不是要删除的结点那么两个指针分别后移,这一步该为pre=pre->next;
p=p->next ;
}
}
}
- 应用实例
#include<stdio.h>
#include<stdlib.h>
//主函数
int main()
{
int arr[7]={5,4,7,3,3,9,0}; //这个是例子。
node*L=creatlist(arr,7),*p;
insert(L,2,12);
del(L,3);
p=L->next;
while(p!=NULL){
printf("%d ",p->data);
p=p->next;
}
}
栈和队列
栈
- 先进后出,后进先出
- 只能在一端(栈顶,Top)进行插入(入栈,Push)和删除(出栈,Pop)
- 基本操作
Stack CreateStack(int MaxSize); //生成空堆栈,其最大长度为MaxSize
int IsFull(Stack S, int MaxSize); //判断堆栈S是否已满
viod Push(Stack S, ElementType item); //将元素item压入堆栈S
int IsEmpty(Stack S); //判断堆栈S是否为空
ElementType Pop(Stack S); //删除并返回栈顶元素
- 栈的表示,数组及单向链表
//数组
//堆栈的定义
#define MaxSize <存储原始的最大个数>
typedef struct
{
ElementType Data[MaxSize];
int Top;
}Stack;
//入栈Push
void Push(Stack *S, ElementType item)
{
if(S->Top == MaxSize - 1)
{
printf("error")
return;
}
else
{
S->Data[++(S->Top)] = item;
return;
}
}
//出栈Pop
ElementType Pop(Stack *S)
{
if(S->Top == -1)
{
printf("error");
return ERROR;
}
else
return (S->Data[(S->Top)--]);
}
//单向链表
//堆栈的定义
typedef struct Node
{
ElementType Data;
struct Node *Next;
}LinkStack;
LinkStack *Top;
//创建一个堆栈
LinkStack *CreateStack()
{
LinkStack *S;
S = (LinkStack *)malloc(sizeof(struct Node));
S->Next = NULL;
}
//判断堆栈是否为空
int IsEmpty(LinkStack *S)
{
return (S->Next == NULL);
}
//入栈Push
void Push(ElementType item, LinkStack *S)
{
struct Node *TmpCell;
TmpCell = malloc(sizeof(struct Node));
TmpCell->Element = item;
TmpCell->Next = S->Next;
S->Next = TmpCell;
}
//出栈Pop
void Pop(LinkStack *S)
{
struct Node *FirstCell;
ElementType result;
if(IsEmpty(S))
{
printf("error");
return NULL;
}
else
{
FirstCell = S->Next;
S->Next = FirstCell->Next;
result = FirstCell->Element;
free(FirstCell);
return result;
}
}
队列
- 先进先出(FIFO,first-in first-out),后进后出
- 只能在一头插入,另一端删除
- 基本操作
Queue CreateQueue(int MaxSize); //生产长度为MaxSize的空队列
int IsFullQ(Queue Q, int MaxSize); //判断队列Q是否已满
void AddQ(Queue Q, ElementType item); //将元素插入队列Q中
int IsEmptyQ(Queue Q); //判断队列Q是否为空
ElementType DeletQ(Queue Q); //将队头元素从队列中删除并返回
- 队列的表示,数组及单向链表
//数组,单向存储
#define MaxSize <存储原始的最大个数>
typedef struct
{
ElementType Data[MaxSize];
int rear;
int front;
}Queue;
//入队
void AddQ(Queue *P, ElementType item)
{
if((P->rear+1)%MaxSize == P->front)
{
printf("Full");
return;
}
P->rear = (P->rear + 1)%MaxSize;
P->Data[P->rear] = item;
}
//出队
ElementType DeleteQ(Queue *P)
{
if(P->front == P->rear)
{
printf("Empty");
return ERROR;
}
else
{
P->front = (P->front+1)%MaxSize;
return P->Data[P->front];
}
}
//链表
typedef struct Node
{
ElementType Data;
struct Node *Next;
}QNode;
typedef struct
{
QNode *rear;
QNode *front;
}LinkQueue;
LinkQueue *P;
//出队
ElementType DeleteQ(LinkQueue *P)
{
QNode *FrontCell;
ElementType FrontElem;
if(P->front == NULL)
{
printf("Empty");
return ERROR;
}
FrontCell = P->front;
if(P->frony == P->rear)
{
P->front = P->rear=NULL;
}
else
{
P->front = P->front->Next;
}
FrontElem = FrontCell->Data;
free(FrontCell);
return FrontElem;
}
树
定义
n(n>=0)个节点构成的有限集合(n=0则空树)
性质
- 子树是互不相交的
- 除了根节点之外,每个节点有且只有一个父节点(根节点没有父节点)
- 一棵树N个节点有N-1条边
基本术语
- 节点的度(degree):节点的子树个数(子树可以超过两个)
- 树的度(degree):树中所有节点中最大的度数(节点的度的最大值)
- 叶节点(leaf):度为0的节点
- 父节点(parent)
- 子节点(child)
- 兄弟节点(sibling)
- 路径和路径长度:路径所包含边的个数
- 祖先节点(ancestor):沿树根到某一节点路径上所有的节点都是
- 子孙节点(descendant):某一节点的子树中的所有节点
- 节点的层次(level):根节点1层,往下逐渐增加
- 树的深度(depth):树种所有节点的最大层次
树的表示
儿子-兄弟表示法
将一般树转化为二叉树
二叉树
- 二叉树有左右之分,而一般的树没有
- 二叉树节点定义
struct BinaryTreeNode
{
int m_nValue;
BinaryTreeNode* m_pLeft;
BinaryTreeNode* m_pRight;
};
特殊二叉树:
- 斜二叉树(链表),0-1-3
- 完美二叉树,0-1-2-3-4-5-6,能有的全有了
- 完全二叉树,0-1-2-3-4-5,最后一层右边的若干个没有了
- 满二叉树,0-1-2-3-4,每个非叶子结点的度都是2
- 二叉搜索树,左子节点小于等于根节点,又子节点大于等于根节点
- 堆(最大堆和最小堆)
- 红黑树
对于任意二叉树
设叶节点(度为0,即没有子树)的数量为n0,度等于1的节点的数量n1,度等于2的节点的数量为n2,则n0 = n2 + 1;
证明:n0 + n1 + n2 - 1 = 二叉树边的数量 = 0 * n0 + 1 * n1 + 2 * n2;
二叉树遍历概述
可以看根节点遍历的位置,判断是那种遍历方式
- 先序遍历(根节点->左子树->右子树)
- 中序遍历(左子树->根节点->右子树)
- 后序遍历(左子树->右子树->根节点)
- 层次遍历/宽度优先遍历(从上到下,从左到右)
//利用递归实现先序遍历
void PreOrderTraversal(BinTree BT)
{
if(BT)
{
printf("%d", BT->Data);
PreOrderTraversal(BT->Left);
PreOrderTraversal(BT->Right);
}
}
//利用递归实现中序遍历
void InOrderTraversal(BinTree BT)
{
if(BT)
{
PreOrderTraversal(BT->Left);
printf("%d", BT->Data);
PreOrderTraversal(BT->Right);
}
}
//利用递归实现后序遍历
void PostOrderTraversal(BinTree BT)
{
if(BT)
{
PreOrderTraversal(BT->Left);
PreOrderTraversal(BT->Right);
printf("%d", BT->Data);
}
}
二叉树存储
1.顺序存储(数组)
对于完全二叉树比较合适,对于一般二叉树需要把空缺的地方补满,浪费空间
2.链表存储
typedef struct TreeNode *BinTree;
typedef BinTree Position;
struct TreeNode
{
ElementType Data;
BinTree Left;
BinTree Right;
}
图
图的遍历
图的表示
1.邻接矩阵
(a) G[N][N],N个顶点,编号为0~N-1,若G[i][j] = 1,则两个点之间存在边,若G[i][j] = 0,则两个点之间不连通;
(b) 直观、简单、好理解,但是如果图稀疏,则浪费空间、时间。
2.邻接表
(a) G[N]为指针数组,对应矩阵每行一个链表,只存非0元素
(b) 稀疏图合算,否则不合算
几个定义
强联通:有向图中顶点V和W之间存在双向路径,则称V和W是强联通的
强联通图:有向图中任意两顶点均强联通
强联通分量:有向图的极大强联通子图
1.深度优先搜索(Depth First Search, DFS)
类似于树的先序遍历,如果用邻接表储存图,时间复杂度为N+E(N个顶点,E条边);如果用用邻接矩阵储存,时间复杂度为N^2。
void DFS(Vertex V)
{
Visited[V] = true;
for(V的每个邻接点W)
{
if(!Visited[W])
{
DFS(W);
}
}
}
2.广度优先搜索(Breadth First Serach, BFS)
类似于树的层序遍历,如果用邻接表储存图,时间复杂度为N+E(N个顶点,E条边);如果用用邻接矩阵储存,时间复杂度为N^2。
void BFS(Vertex V)
{
Visited[V] = true;
Enqueue(V,Q);
while(!IsEmpty(Q))
{
V = Dequeue(Q);
for(V的每个邻接点W)
{
if(!Visited[W])
{
Visited[W] = true;
Enqueue(W,Q);
}
}
}
}
最短路径问题
1.单源最短路径问题:从固定源点出发
a. 无权图的单源最短路径算法
按照递增(非递减)的顺序找到各个顶点的最短路
void Unweight(Vertex S)
{
Enqueue(S, Q);
while(!IsEmpty(Q))
{
V = Dequeue(Q);
for(V的每个邻接点W)
{
if(dist[W] == -1)
{
dist[W] = dist[V] + 1;
path[W] = V;
Enqueue(W,Q);
}
}
}
}
b. 有权图的单源最短路径算法
按照递增的顺序找到各个顶点的最短路
//不能解决有边的权重为负数的情况
void Dijkstra(Vertex s)
{
while(1)
{
V = 未收录顶点中dist最小者;
if(这样的V不存在)
{
break;
}
collected[V] = true;
for(V的每个邻接点W)
{
if(collected[W] == false)
{
if(dist[V]+E<v,w> < dist[W])
{
dist[W] = dist[V]+E<v,w>;
path[W] = V;
}
}
}
}
}
2.多源最短路径问题:求任意两顶点之间的最短路径
方法1:直接讲但愿最短路算法调用|V|遍,时间复杂度为O(|V|^3+|E|*|V|)
方法2:Floyd算法,时间复杂度为O(|V|^3)
void Floyd()
{
for(int i=0; i<N; i++)
{
for(int j=0; j<N; j++)
{
D[i][j] = G[i][j];
path[i][j] = -1;
}
}
for(int k=0; k<N; k++)
{
for(int i=0; i<N; i++)
{
for(int j=0; j<N; j++)
{
if(D[i][k]+D[k][j]<D[i][j])
{
D[i][j] = D[i][k]+D[k][j];
path[i][j] = k;
}
}
}
}
}