数据结构与算法分析复习笔记(持续更新...欢迎指正)

数据结构与算法分析

文章目录


前言

复习408考研数据结构和学习蓝桥杯(《挑战程序设计竞赛》的初级篇)的笔记,也有相关的离散数学基础内容记录,本笔记以应试为主,数据结构的基础部分将使用C/C++和java的可执行代码实现

对于每种数据结构与算法,都应该在理解其基础的情况下,将它的模板背过(找几道典型的题目,一开始不会写,就看懂答案后当做例题背过模板,再自己重新写出,重复以上操作,直到自己可以独立写出题目)——数学题也是这样学习的

如果跟着学校,就会被安排的明明白白,无论你学校的好坏,基本都没救了

CS学习之路只能靠自己!


持续更新ing

一、数据结构概述

数据结构相关基本概念

1. 数据

信息的载体,是描述客观事物的符号,是计算机中可以操作的对象,是能被计算机识 别,并输入给计算机处理的符号集合。

比如我们现在常用的搜索引擎,一般会有网页、图片、视频等分类。 MP3 就是声音数据,图片当然是图像数据,视频就不用说了,而网页其实指的就是全部数 据的搜索,包括最重要的数字和字符等文字数据。

数据最终会以01二进制形式被计算机识别

2. 数据元素

是组成数据的、有一定意义的基本单位,在计算机中通常作为整体处理。 也被称为记录。

比如,在人类中,什么是数据元素呀? 当然是人了。
畜类呢? 牛、马、羊、鸡、猪、 狗等动物当然就是畜类的数据元素。

3. 数据项

一个数据元素可以自若干个数据项组成。

比如人这样的数据元素,可以有眼、耳、鼻、嘴、 手、脚这些数据项,也可以有 姓名、年龄、性别、出生地址、联系电话等数据项,具体有哪些数据项,要视你做的 系统来决定。

数据项是数据不可分割的最小单位。在数据结构这门课程中,我们把数据项定义 为最小单位,是有助于我们更好地解决问题。所以,记住了,数据项是数据的最小单 位。但真正讨论问题时,数据元素才是数据结构中建立数据模型的着眼点。就像我们 讨论一部电影时,是讨论这部电影角色这样的"数据元素",而不是针对这个角色的姓 名或者年龄这样的"数据项"去研究分析。

4. 数据对象

是性质相同的数据元素的集合,是数据的子集。

什么叫性质相同呢,是指数据元素具有相同数量和类型的数据项,比如,还是刚才的例子,人都有姓名、生日、性别等相同的数据项。

既然数据对象是数据的子集,在实际应用中,处理的数据元素通常具有相同性质 , 在不产生混淆的情况下,我们都将数据对象简称为数据

5. 数据结构

是相互之间存在一种或多种特定关系的数据元素的集舍。

结构,简单的理解就是关系,比如分子结构,就是说组成分子的原子之间的排列方式。严格点说, 结构是指各个组成部分相互搭配和排列的方式,在现实世界中,不同数据元素之间不是独立的,而是存在特定的关系,我们将这些关系称为结构。

按照视点的不同, 我们把数据结构分为逻辑结构和物理结构。

i. 逻辑结构:是指数据对象中数据元素之间的相互关系

• 集合结构
• 线性结构
• 树形结构
• 图形结构

ii. 物理结构 (很多书中也叫做存储结构,你只要在理解上把官们当一回事就可以了)
是指数据的逻辑结构在计算机中的存储形式。那么根据物理结构的定义,实际上就是如何把数据元素 存储到计算机的存储器中

• 顺序存储
• 链式存储

6. 程序结构

• 顺序结构
• 分支结构
• 循环结构

7. 数据类型

是指一组性质相同的值的集合及定义在此集合上的一些操作的总称。

在 C 语言中,按照取值的不同,数据类型可以分为两

• 原子类型:是不可以再分解的基本类型,包括整型、实型、字符型等。(浮点数计算有误差!!!)
• 结构类型:自若干个类型组合而戚,是可以再分解的。例如,整型数组是由若干整型数据组成的。

在这里插入图片描述

8. 算法

• 算法具有五个基本特性: 输入、输出、 有穷性、确定性和可行性
• 算法设计的要求:正确性、可读性、健壮性、时间效率高和存储量低

在这里插入图片描述

基本结构简介

1.从集合到结构体

朴素集合论(最原始的集合论)中的定义,即集合是“确定的一堆东西”,集合里的“东西”则称为元素。现代的集合一般被定义为:由一个或多个确定的元素所构成的整体 。

C 数组允许定义可存储相同类型数据项的变量,结构是 C 编程中另一种用户自定义的可用的数据类型,它允许您存储不同类型的数据项。

结构体就是集合在C中的表示,几乎所有的数学概念都是建立在集合的基础上,所有的数据结构都是特殊的集合,所以在C中定义数据结构,必须使用结构体。

代码如下(示例):

//此声明声明了拥有3个成员的结构体,分别为整型的a,字符型的b和双精度的c
//同时又声明了结构体变量s1
//这个结构体并没有标明其标签
struct 
{
    int a;
    char b;
    double c;
} s1;
 
//此声明声明了拥有3个成员的结构体,分别为整型的a,字符型的b和双精度的c
//结构体的标签被命名为SIMPLE,没有声明变量
struct SIMPLE
{
    int a;
    char b;
    double c;
};
//用SIMPLE标签的结构体,另外声明了变量t1、t2、t3
struct SIMPLE t1, t2[20], *t3;
 
//也可以用typedef创建新类型
typedef struct
{
    int a;
    char b;
    double c; 
} Simple2;
//现在可以用Simple2作为类型声明新的结构体变量
Simple2 u1, u2[20], *u3;

//此结构体的声明包含了其他的结构体
struct COMPLEX
{
    char string[100];
    struct SIMPLE a;
};

// --------------------------------------------------------------

//此结构体的声明包含了指向自己类型的指针
struct NODE
{
    char string[100];
    struct NODE *next_node;
};

// --------------------------------------------------------------
//结构体变量的初始化

#include <stdio.h>
struct Books
{
   char  title[50];
   char  author[50];
   char  subject[100];
   int   book_id;
} book = {"C 语言", "RUNOOB", "编程语言", 123456}; //从这里的 book = {...}也可以看出,结构体就是集合

 
int main()
{
    printf("title : %s\nauthor: %s\nsubject: %s\nbook_id: %d\n", book.title, book.author, book.subject, book.book_id);
}


2.映射、函数、算法

函数也称映射(两个元素的集之间元素相互“对应”的关系)或变换,函数也是一种算法、操作、算符,映射在计算机中处处存在

对数据结构所有的操作可以归纳为创建、销毁、增删查改


3.线性结构、树形结构、图形结构

数列、级数、线性表都是序列

顺序表和单链表是重中之重,是后续所有数据结构的基础,必须熟练的掌握,但经过实践学习,很多时候思路是容易的,但是无法写出,都是由于C语言指针、结构体没有掌握,语言基础薄弱。所以在接下来继续学习的路上,我还会花费大量的时间打牢C的基础,完成基础的学习,待熟练之后,再继续推进


算法性能评价

1.算法的时间复杂度(无穷大之间的比阶—等价无穷大)

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

我们给出这样的定义,输入规模 n 在没有限制的情况下,只要超过一个数值 N ,这个函数就总是大于另一个函数,我的称函数是渐近增长的。
在这里插入图片描述
在这里插入图片描述

常对幂指阶
0(1) < O(logn) < O(n) < O(nlogn) < 0(n2 ) < 0(n3 ) < 0(2") < O(n!) < O(n")

只保留阶数最高项!!!即数量级!!!

忽略常数项,忽略系数,系数化
在这里插入图片描述
例题:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
例题:
在这里插入图片描述
核心语句 m++执行的次数为:(注意双重求和)
在这里插入图片描述
在这里插入图片描述

2.算法的空间复杂度

声明变量带来的内存开销

例题:
在这里插入图片描述
例题:
在这里插入图片描述
例题:
在这里插入图片描述
例题:
在这里插入图片描述

函数递归调用带来的内存开销

例题:
在这里插入图片描述
在这里插入图片描述
例题:
在这里插入图片描述
在这里插入图片描述

二、线性结构

C语言基础

1.数组

C 传递数组给函数

如果想要在函数中传递一个一维数组作为参数,您必须以下面三种方式来声明函数形式参数,这三种声明方式的结果是一样的,因为每种方式都会告诉编译器将要接收一个整型指针。同样地,您也可以传递一个多维数组作为形式参数。

方式 1
形式参数是一个指针(您可以在下一章中学习到有关指针的知识):

void myFunction(int *param)
{
.
.
.
}

方式 2
形式参数是一个已定义大小的数组:

void myFunction(int param[10])
{
.
.
.
}

方式 3
形式参数是一个未定义大小的数组:

void myFunction(int param[])
{
.
.
.
}

实例
现在,让我们来看下面这个函数,它把数组作为参数,同时还传递了另一个参数,根据所传的参数,会返回数组中各元素的平均值:

double getAverage(int arr[], int size)
{
  int    i;
  double avg;
  double sum;
 
  for (i = 0; i < size; ++i)
  {
    sum += arr[i];
  }
 
  avg = sum / size;
 
  return avg;
}

现在,让我们调用上面的函数,如下所示:

实例

#include <stdio.h>
 
/* 函数声明 */
double getAverage(int arr[], int size);
 
int main ()
{
   /* 带有 5 个元素的整型数组 */
   int balance[5] = {1000, 2, 3, 17, 50};
   double avg;
 
   /* 传递一个指向数组的指针作为参数 */
   avg = getAverage( balance, 5 ) ;
 
   /* 输出返回值 */
   printf( "平均值是: %f ", avg );
    
   return 0;
}
 
double getAverage(int arr[], int size)
{
  int    i;
  double avg;
  double sum=0;
 
  for (i = 0; i < size; ++i)
  {
    sum += arr[i];
  }
 
  avg = sum / size;
 
  return avg;
}

当上面的代码被编译和执行时,它会产生下列结果:

平均值是: 214.400000
您可以看到,就函数而言,数组的长度是无关紧要的,因为 C 不会对形式参数执行边界检查。

例题:使用数组( [蓝桥杯2015初赛]星系炸弹)

题目描述 在X星系的广袤空间中漂浮着许多X星人造“炸弹”,用来作为宇宙中的路标。 每个炸弹都可以设定多少天之后爆炸。
比如:阿尔法炸弹2015年1月1日放置,定时为15天,则它在2015年1月16日爆炸。
有一个贝塔炸弹,a年b月c日放置,定时为n天,请你计算它爆炸的准确日期。

输入
输入存在多组数据,每组数据输入一行,每一行输入四个正整数a,b,c,n
输入保证日期在1000-01-01到2020-01-01之间,且日期合法。 n不超过1000

输出
请填写该日期,格式为 yyyy-mm-dd 即4位年份2位月份2位日期。比如:2015-02-19
请严格按照格式书写。不能出现其它文字或符号。

样例输入 Copy
2015 1 1 15
2014 11 9 1000

样例输出 Copy
2015-01-16
2017-08-05

#include <iostream>
using namespace std;
#include <cstdio>

bool fff(int year) {
	if (year % 4 == 0 && year % 100 != 0 || year % 400 == 0) {
	//分闰年(29天;4年1闰;闰年能被4整除但不能被100整除,或能被400整除,即四年一闰,百年不闰,四百年再闰。 例如,2000年是闰年,2100年则是平年)
		return true;
	}
	//平年(28天)
	return false;
}

int main() {
	int ms[13] = {0, 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31};
	//数组第一位下标为0,只也为0,这样数组下标就可以与月份相同,便于书写
	int y, m, d, n;
	while (cin >> y >> m >> d >> n) {
		//将n拆成一天一天的加,满一个月(年)就进位
		for (int i = 0; i < n; i++) {
			//判断现在的年份是否为闰年,修改2月的天数
			if (fff(y)) {
				ms[2] = 29;
			} else {
				ms[2] = 28;
			}
			
			//满一个月,进一月
			if (++d > ms[m]) {
				m++;
				d = 1;
			}
			
			//满一年,进一个年
			if (m > 12) {
				y++;
				m = 1;
			}
		}
		printf("%d-%02d-%02d\n", y, m, d);//%02d表示占两个位置,如果位置不够,就用0来占。
	}
	return 0;
}


2.指针

#include <stdio.h>
 
int main ()
{
    int var_runoob = 10;
    int *p;              // 定义指针变量
    p = &var_runoob;	//p就是指针,而不能认为*p是指针;*p是p所指向的变量
 
   printf("var_runoob 变量的地址: %p\n", p);
   return 0;
}

当上面的代码被编译和执行时,它会产生下列结果:

var_runoob 变量的地址: 0x7ffeeaae08d8

递增一个指针
我们喜欢在程序中使用指针代替数组,因为变量指针可以递增,而数组不能递增,数组可以看成一个指针常量。下面的程序递增变量指针,以便顺序访问数组中的每一个元素:

实例

#include <stdio.h>
 
const int MAX = 3;
 
int main ()
{
   int  var[] = {10, 100, 200};
   int  i, *ptr;
 
   /* 指针中的数组地址 */
   ptr = var; //将数组的首地址赋值给指针ptr
   
   for ( i = 0; i < MAX; i++)
   {
      printf("存储地址:var[%d] = %p\n", i, ptr );
      printf("存储值:var[%d] = %d\n", i, *ptr );
      
	/* 指向下一个位置 */
      ptr++;//ptr指向一片连续的内存(即数组),所以可以进行递增或递减运算
   }
   return 0;
}

当上面的代码被编译和执行时,它会产生下列结果:

存储地址:var[0] = e4a298cc
存储值:var[0] = 10
存储地址:var[1] = e4a298d0
存储值:var[1] = 100
存储地址:var[2] = e4a298d4
存储值:var[2] = 200

3.数组与指针

数组名即首地址,数组名就是一个指针常量

double balance[50];

balance 一个指向 &balance[0] 的指针,即数组 balance 的第一个元素的地址。因此,下面的程序片段把 p 赋值为 balance 的第一个元素的地址:

double *p;
double balance[10];
p = balance;

使用数组名作为常量指针是合法的,反之亦然。因此,*(balance + 4) 是一种访问 balance[4] 数据的合法方式。

指针类型的数组(指针组成的数组)

可能有一种情况,我们想要让数组存储指向 int 或 char 或其他数据类型的指针。下面是一个指向整数的指针数组的声明:

int *ptr[MAX];

在这里,把 ptr 声明为一个数组,由 MAX 个整数指针组成。因此,ptr 中的每个元素,都是一个指向 int 值的指针。下面的实例用到了三个整数,它们将存储在一个指针数组中,如下所示:

实例

#include <stdio.h>
 
const int MAX = 3;
 
int main ()
{
   int  var[] = {10, 100, 200};
   int i, *ptr[MAX];
 
   for ( i = 0; i < MAX; i++)
   {
      ptr[i] = &var[i]; /* 赋值为整数的地址 */
   }
   for ( i = 0; i < MAX; i++)
   {
      printf("Value of var[%d] = %d\n", i, *ptr[i] );
   }
   return 0;
}

当上面的代码被编译和执行时,它会产生下列结果:

Value of var[0] = 10
Value of var[1] = 100
Value of var[2] = 200

您也可以用一个指向字符的指针数组存储字符串列表,如下:

实例

#include <stdio.h>
 
const int MAX = 4;
 
int main ()
{
   const char *names[] = {
                   "Zara Ali",
                   "Hina Ali",
                   "Nuha Ali",
                   "Sara Ali",
   };
   int i = 0;
 
   for ( i = 0; i < MAX; i++)
   {
      printf("Value of names[%d] = %s\n", i, names[i] );
   }
   return 0;
}

当上面的代码被编译和执行时,它会产生下列结果:

Value of names[0] = Zara Ali
Value of names[1] = Hina Ali
Value of names[2] = Nuha Ali
Value of names[3] = Sara Ali

能接受指针作为参数的函数,也能接受数组作为参数

如下所示:

实例

#include <stdio.h>
 
/* 函数声明 */
double getAverage(int *arr, int size);
 
int main ()
{
   /* 带有 5 个元素的整型数组  */
   int balance[5] = {1000, 2, 3, 17, 50};
   double avg;
 
   /* 传递一个指向数组的指针作为参数 */
   avg = getAverage( balance, 5 ) ;
 
   /* 输出返回值  */
   printf("Average value is: %f\n", avg );
    
   return 0;
}

double getAverage(int *arr, int size)
{
  int    i, sum = 0;       
  double avg;          
 
  for (i = 0; i < size; ++i)
  {
    sum += arr[i];
  }
 
  avg = (double)sum / size;
 
  return avg;
}

当上面的代码被编译和执行时,它会产生下列结果:

Average value is: 214.40000

**注意:**不能直接传递数组int balance[],因为int balance[]是一个数组,只有数组名才是指针

4.结构体指针

指向结构的指针
您可以定义指向结构的指针,方式与定义指向其他类型变量的指针相似,如下所示:

struct Books *struct_pointer;

现在,您可以在上述定义的指针变量中存储结构变量的地址。为了查找结构变量的地址,请把 & 运算符放在结构名称的前面,如下所示:

struct_pointer = &Book1;

为了使用指向该结构的指针访问结构的成员,您必须使用 -> 运算符(自带解引用),如下所示:

struct_pointer->title;

在这里插入图片描述

顺序表

1. 顺续表的静态分配(普普通通的数组~)

#include <stdio.h>
#include <stdlib.h>
#define MaxSize 10
typedef struct {
	int data[MaxSize];//用静态的“数组”存放元素 
	int length;//顺序表的当前长度 
}SqList;

//基本操作:创建并初始化一个顺序表 
void InitList(SqList *L) {
	for(int i=0;i<MaxSize;i++){
		L->data[i]=0; //默认初始化顺序表元素都为0 
		L->length=0; //默认初始化顺序表长度为0 
	}
}

int main() {
	SqList *L; //声明一个顺序表指针L 
	InitList(L);//将指针L传入初始化函数 
	return 0;
}

2. 顺续表的动态分配(可变数组~在空间不够时,再开辟一块更大的空间)

#include <stdio.h>
#include <stdlib.h>
#define InitSize 10 //默认的最大长度 
typedef struct {
	int *data;//数组名data作为动态分配数组的指针
	int MaxSize;//顺序表的最大容量 
	int length;//顺序表的当前长度 
}SqList;

//基本操作:创建并初始化一个顺序表 
void InitList(SqList *L) {
	//用malloc()申请一片连续的内存空间,使data指向申请空间的首地址 
	L->data=(int*)malloc(InitSize*sizeof(int));
	L->MaxSize=InitSize;
	L->length=0;	 
}

//基本操作:动态增加数组的长度 (可变数组) 
void IncreaseSize(SqList *L,int len) {
	//让p指向data,即可变数组的首地址,目的是为了copy一份L,未来可以通过free(p)来回收原来L的内存 
	int *p=L->data;
	//新分配一块更大的内存空间 
	L->data=(int*)malloc((L->MaxSize+len)*sizeof(int));
	
	//更新L->length的大小
	L->length= L->length+len; 
	
	//将原来的数据copy到新分配的空间里
	for(int i=0;i<L->length;i++) {
		L->data[i]=p[i];
	} 
	L->MaxSize=L->MaxSize+len;
	//释放原来可变数组的内存 
	free(p);
}
 
int main() {
	SqList *L; //声明一个顺序表指针L 
	InitList(L);//将指针L传入初始化函数 
	IncreaseSize(L,5);//将可变数组长度加5 
	return 0;
}

3. 顺续表的基本操作

插入

bool ListInsert(SqList *L,int i,int e) {
	//将e插入到顺序表L中第i个位置
	if(i<1||i>L->length+1)	//判断i是否合法 
		return false; 
	if(L->length>=MaxSize)	//当前存储空间已满,不能插入 
		return false; 	
	for(int j=L->length;j>=i;j--) {
		L->data[i-1]=L->data[j-1];	//将第i个元素及其之后的元素后移 
	}
	L->data[i-1]=e;			//插入e
	L->length++;
	return true; 
}

在这里插入图片描述
例题:
在这里插入图片描述
在这里插入图片描述

删除

bool ListDelete(SqList *L,int i,int e) {
	//删除顺序表L中第i位元素
	if(i<1||i>L->length)	//判断i是否合法 
		return false;  
	e=L->data[i-1];			//将被删除的元素赋值给e
	for(int j=i;j<L->length;j++) {
		//将第i个位置之后的元素前移
		L->data[j-1]=L->data[j];
	}
	L->length--;			//表长-1 
	return true; 
}

在这里插入图片描述

按值查找(顺序查找)

int LocateElem(SqList *L,int e) {
	//按值查找顺序表中元素e,如果成功,就返回元素位序,否则返回0
	for(int i=0;i<L->length;i++) {
		if(L->data[i]==e) return i+i;	//下标为i的元素=e,返回其位序i+1 
	} 
	return 0;						//查找失败 
}

在这里插入图片描述
例题1:存取就是查找(概念)
在这里插入图片描述
在这里插入图片描述
例题2:存取就是查找(应用)
在这里插入图片描述
在这里插入图片描述

单链表

由于顺序表的插入、删除操作需要移动大量的元素,影响效率,故引入链表:对线性表的插入、删除就不需要移动元素了,只需要修改指针;但是查找操作就需要从头开始查找线性表
在这里插入图片描述

  • 头指针:代表了链表本身LNode* 类型就是LinkList类型,在强调“结点”是,用LNode*,在强调“链表”时,用LinkList
  • 头结点:链表第一个位置操作与其他地方操作统一;空表与非空表操作统一,带头结点的空链表就是一个头结点
    在这里插入图片描述

1. 单链表的基本操作

#include<stdio.h>
#include<stdlib.h>

typedef struct LNode {
	int data;	//数据域 
	struct LNode* next;		//指向下一个节点的指针域 
}LNode, *LinkList;

//基本操作:初始化一个空的(带头结点)单链表 
bool InitList(LinkList L) {
	L = (LNode*)malloc(sizeof(LNode));
	if(L == NULL)	//空间不足,分配失败 
		return false;
	else 
		L->next = NULL;	//头结点之后还没有结点 
} 

//基本操作:(后插法)按位序插入(带头结点)
bool ListInsert(LinkList L,int i,int e) {
	if(i<1) {		//位序i不合法 
		return false;
	} 
	LNode *p = L;	//指针指向当前扫描的结点 
	int j = 0;		//当前指针指向第几个结点
	while(p!=NULL&&j<i-1) {	//循环找到第i-1个结点 
		p=p->next;
		j++; 
	} 
	if(p==NULL) {	//i值不合法 
		return false;
	} 
	else 
		LNode* s = (LNode *)malloc(sizeof(LNode));
		s->data=e;
		s->next=p->next;
		p->next=s;
		return true;
}

//基本操作:(前插法)按位序插入(带头结点)
bool InsertPriorNode(LinkList L,LNode *p,int e) {
	if(p==NULL) {
		return false;
	}
	LNode *s = (LNode *)malloc(sizeof(LNode));
	if(s==NULL) //内存分配失败
		return false;
	s->next=p->next;
	p->next=s;
	s->data=p->data;
	p->data=e;
	return true;
}

//基本操作:(前插法)按位序删除(带头结点)
bool ListDelete(LinkList L,int i,int e) {
	if(i<1)
		return false;
	else
		LNode *p=L;	//指针p指向头结点 
		int j=0;	//当前结点指向第几结点
		while(p!=NULL&j<i-1) {
			p=p->next;
			j++;
		} 
		if(p==NULL)	//i值不合法 
			return false;
		if(p->next=NULL)	//第i-1个结点后已经没有其他结点
			return false;
		LNode *q=p->next;	//p找到应删除的结点后,用q指向被删除结点
		e = q->data;	//用e返回元素的值
		p->next=q->next;	//断开
		free(p);
		return true; 
}

//基本操作:头插法建立单链表(带头结点)
//应用:单链表的逆置 
LinkList List_HeadInsert(LinkList L) {
	LNode *s;
	int x;	//输入数据 
	L=(LinkList)malloc(sizeof(LNode));//创建头结点 
	scanf("%d",&x);
	while(x!=9999) {						//输入9999表示结束 
		s=(LNode *)malloc(sizeof(LNode));//创建新结点 
		s->data=x;
		s->next=L->next;
		L->next=s;
		scanf("%d",&x); 
	} 
	return L;
} 


//基本操作:尾插法建立单链表(带头结点)
LinkList List_TailInsert(LinkList L) {
	int x;
	L=(LNode *)malloc(sizeof(LNode));//创建头结点
	LNode *s,*r=L	//创建头尾指针
	scanf("%d",&x); 
	while(x!=9999) {
		s=(LNode *)malloc(sizeof(LNode));//创建新结点
		s->data=x;
		r->next=s;
		r=s;
	scanf("%d",&x); 
	} 
	r->next=NULL;
	return L;	
}

int main() {
	
} 

2. 双向链表

#include<stdio.h>
#include<stdlib.h>

typedef struct LNode {
	int data;	//数据域 
	struct LNode *prior,*next;		//指向下一个节点的指针域 
}DNode, *DLinkList;

//基本操作:初始化双链表
bool InitDLinkList(DLinkList L) {
	L=(DNode*)malloc(sizeof(DNode));//创建头结点
	if(L==NULL) {
		return false;
	} 
	else
	L->prior=NULL;
	L->next=NULL;
	 	return true;
}

//基本操作:双链表的后插法
bool InsertNextDNode(DNode *p,DNode *s) {//将结点*s插到p后
	if(p==NULL || s==NULL) return false;
	s->next=p->next;
	if(p->next !=NULL)
	p->next->prior=s;
	s->prior=p;
	p->next=s;
	return true; 
}

int main() {
	
} 

3. 循环链表

#include<stdio.h>
#include<stdlib.h>

typedef struct LNode{
	int data;
	struct LNode *next;
}LNode,*LinkList;

//初始化循环单链表
bool InitList(LinkList L) {
	L=(LNode*)malloc(sizeof(LNode));//创建头结点
	if(L==NULL) return false;
	L->next=L;//头结点next指向头结点
	return true;
} 

int main() {
	
}

4. 双向循环链表

5. 线性表的存储结构选择

例题:算法复杂度
在这里插入图片描述
在这里插入图片描述
例题:线性表的存储结构选择
在这里插入图片描述
在这里插入图片描述
例题:判空条件
在这里插入图片描述
在这里插入图片描述
例题:线性表的存储结构选择
在这里插入图片描述
在这里插入图片描述
例题:线性表的存储结构选择
在这里插入图片描述
在这里插入图片描述
例题:链接地址(next域中存储了什么)
在这里插入图片描述
在这里插入图片描述
例题:线性表的存储结构选择
在这里插入图片描述
在这里插入图片描述

栈的抽象数据类型是由以下结构和操作定义的。堆栈是结构化的,如上面所描述的,栈是一个有序的项的集,项添加和删除的一端称为“顶”。栈的命令是按后进先出进行的。栈的操作如下:

Stack()创建一个新的空栈。它不需要参数,并返回一个空栈。
Push(item)将新项添加到堆栈的顶部。它需要参数item并且没有返回值。
pop()从栈顶删除项目。它不需要参数,返回item。栈被修改。
peek()返回栈顶的项,不删除它。它不需要参数。堆栈不被修改。
isEmpty()测试看栈是否为空。它不需要参数,返回一个布尔值。 size()返回栈的项目数。它不需要参数,返回一个整数。

1. 顺序栈

#include<stdio.h>
#include<stdlib.h>
#define MaxSize 10 

//顺序栈的定义 
typedef struct {
	int data[MaxSize];
	int top;
}SqStack;

//初始化顺序栈 
void InitStack(SqStack *s) {
	s->top=-1;	//初始化栈顶指针(标记)
} 

//新元素入栈
bool Push(SqStack *s,int x) {
	if(s->top==MaxSize-1) return false;
	else {
		s->top=s->top+1;
		s->data[s->top]=x;
		return true;
	}
}

//出栈操作
 bool Pop(SqStack *s,int x) {
	if(s->top==-1) return false;
	else {
		x=s->data[s->top];
		s->top=s->top-1;
		return true;
	}
}

int main() {
	
}

2. 链栈(同链表,进栈就是头插法,出栈就是头删法,掌握了以上的本质,就可以根据实际情况的要求建模)

队列

1. 余数—周期—分组

例题:星期数
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
例题:大数乘方
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
例题:循环队列
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

对于循环队列取余运算的总结:

以本题为例:
静态数组的MaxSize=10,即数组长度大小一定

一开始插入三个元素,在逻辑上不看做循环队列也是也可以的,如下图:
在这里插入图片描述
但是当前4个元素出队列了,之后又新增了2个元素入队列(如下图),就会产生假溢出现象,即数组并没有完全利用,但是已经无法再入队了。

这时如果将队列在逻辑上看做循环队列,如下图,则rear指针可以从头再次添加队尾元素,由取余运算的意义:MaxSize=10等价于头尾指针可以在(0,1,2,3,4,5,6, 7,8,9)序列内循环的取值,这时就可以无视头尾指针在真正的数组上的前后顺序,它们在逻辑上(循环队列)永远是有先后顺序的。
在这里插入图片描述

2. 顺序队列

//定义顺序队列 
typedef struct {
	int data[MaxSize];
	int front,rear;
}SqQueue;

//初始化队列
void InitQueue(SqQueue *Q) {
	//初始时 队头、队尾指针指向0
	Q->rear=0;
	Q->front=0; 
} 

//入队 
bool EnQueue(SqQueue *Q,int x) {
	if((Q->rear+1)%MaxSize==Q->front)	//队满 
		return false;
	else
		Q->data[Q->rear]=x;//将x插入队尾
		Q->rear=(Q->rear+1)%MaxSize;//队尾指针后移 (逻辑上看做循环队列)
	return true;
}

//出队
bool DnQueue(SqQueue *Q,int x) {
	if(Q->rear==Q->front) return false;//队空则报错
	else 
		x=Q->data[Q->front];
		Q->front=(Q->front+1)%MaxSize;
	return true;
}

3. 链队列

#include<stdio.h>
#include<stdlib.h>
#define MaxSize 10
//链队列就是操作上受限的单链表
//链队列的定义
typedef struct LNode {		//链队列的结点
	int data;
	struct LNode* next;
}LinkNode;

typedef struct {			//链式队列 
	LinkNode *front,*rear;	//队列的队头和队尾指针 s
}LinkQueue;

//初始化(带头结点)
void InitQueue(LinkQueue *Q) {
	Q->front=Q->rear=(LinkNode*)malloc(sizeof(LinkNode));
	Q->front->next=NULL;
}

//队列判空(带头结点)
bool IsEmpty(LinkQueue Q) {
	if(Q.front==Q.rear) return true;
	else return false;
}

//入队(带头结点)
void EnQueue(LinkQueue *Q,int x) {
	LinkNode *s=(LinkNode*)malloc(sizeof(LinkNode));
	s->data=x;
	s->next=NULL;
	Q->rear->next=s;
	Q->rear=s;
}

//出队(带头结点)
bool DeQueue(LinkQueue *Q,int x) {
	if(Q->front==Q->rear) return false; //空队列
	else {
		LinkNode *p=Q->front->next;		//用p指向待删除的结点 
		x=p->data;						//用x值返回删除结点的值
		Q->front->next=p->next;			//修改头结点的next指针
		if(Q->rear==p)  				//本次为最后一个结点出队
			Q->rear=Q->front;			//修改rear 
		free(p);
	}
	return true;
}


//------------------------------------------------------------------ 

//初始化(不带头结点)
void InitQueue2(LinkQueue *Q) {
	Q->front=NULL;
	Q->rear=NULL;
}

//队列判空(不带头结点)
bool IsEmpty2(LinkQueue Q) {
	if(Q.front==NULL)  return true;
	else return false;
}

//入队(不带头结点)
void EnQueue2(LinkQueue *Q,int x) {
	LinkNode *s=(LinkNode*)malloc(sizeof(LinkNode));
	s->data=x;
	s->next=NULL;
	if(Q->front==NULL) {	//空队列中插入第一个元素
		Q->front=s;
		Q->rear=s;
	} else {
		Q->rear->next=s;	//新结点插到rear之后 
		Q->rear=s;
	}
}

//出队(不带头结点)
bool DeQueue2(LinkQueue *Q,int x) {
	if(Q->front==NULL) return false;	//空队
	LinkNode *p=Q->front;				//p指向本次出队的结点 
	x=p->data;							//x值返回删除结点的值
	Q->front=p->next;					//修改front 
	if(Q->rear==p) {					//此次为最后一个结点 
		Q->front=NULL;
		Q->rear=NULL;
	}
	free(p);
	return true;
}

int main() {
	
} 

4. 双端队列(考点:判断输出序列合法性)

在这里插入图片描述

串与KMP匹配算法

特殊状态的枚举

剪枝

三、贪心法

四、动态规划

五、查找

1. 顺序查找

//顺序查找(线性查找),主要用于线性表的查找,顺序存储与链式存储均可以 
//无序线性表的顺序查找(哨兵)
typedef struct {
	int *elem;	//用指针表示数组 
	int TableLen;	//线性表长度 
}SSTable; 

int Search_Seq(SSTable ST,int key) {
	ST.elem[0]=key;	//线性表第一个设为查找关键字,即哨兵
	for(int i=ST.TableLen;ST.elem[i]!=key;i--) {
		return i;
	} 
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

2. 折半查找

//折半查找(二分查找),仅适用于有序顺序表(仅适用于数组) 
typedef struct {
	int data[MaxSize];
	int Length;
}SeqList;

int Binary_Search(SeqList L,int key) {
	int low=0;
	int high=L.Length-1;
	int mid;
	
	while(low<=high) {
		mid=(low+high)/2;
		if(L.data[mid]==key) return mid;
		else if(L.data[mid]>key) high=mid-1;
		else low=mid+1;
	}
	return -1;
}

在这里插入图片描述

3. 分块查找

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

4. B树

5. B+树

6. 散列表与散列函数

散列表的概念

在这里插入图片描述

散列函数的构造方法

在这里插入图片描述

直接定址法

在这里插入图片描述

除留取余法

在这里插入图片描述

数字分析法

在这里插入图片描述

平方取中法与折叠法

在这里插入图片描述

散列函数的冲突处理

在这里插入图片描述

开放定址法

在这里插入图片描述

线性探测法与堆积现象

在这里插入图片描述

平方探测法、再散列法、伪随机法

在这里插入图片描述

拉链法

开放定址法的弊端在这里插入图片描述
引入拉链法
在这里插入图片描述
将所有冲突的元素放在链表后
在这里插入图片描述

散列表查找总结

在这里插入图片描述
在这里插入图片描述

六、排序

在这里插入图片描述

1. 直接插入排序

在这里插入图片描述
在这里插入图片描述

void InsertSort(int A[],int n) {
	int i,j;
	for(i=2;i<=n;i++) {
		A[0]=A[j];//哨兵
		for(j=i-1;A[0].key<A[j].key;j--)
			A[j+1]=A[j];
		A[j+1]=A[0];
	}
}

例题

首先A【0】处空出来(哨兵),此时待插入的数字为A【2】(i<=2开始的);
在这里插入图片描述
将A【2】的值6赋值到A【0】处(哨兵);接着进入了第二层for循环,此时
j=1;故比较哨兵与A【1】的值,结果没有发生变化;
在这里插入图片描述
第二趟排序,将7插入到哨兵的位置(此时i=3);进入第二层for循环,此时j=2,即比较6与7的大小,任然没有变化,跳出for循环
在这里插入图片描述
第三趟排序,进入第二层for循环,发现2均小于已经排好序的序列元素,故一直元素往后移动,直到A【0】.key<A【j】.key
在这里插入图片描述
在这里插入图片描述
最后进行赋值操作A[j+1]=A[0];剩下的仿上可得解
在这里插入图片描述
在这里插入图片描述

2. 折半插入排序

在这里插入图片描述
直接插入排序是在顺序查找的基础上进行的排序;将顺序查找修改为折半查找就得到了折半插入排序

3. 希尔(插入)排序

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
步长选择:
在这里插入图片描述

4. 冒泡(交换)排序

在这里插入图片描述

void BubbleSort(int A[],int n) {
	for(int i=0;i<n-1;i++) {
		bool flag=false;
		for(int j=n-1;j>i;j--) //从后向前比较
			//判断两个相邻的元素是不是逆序
			if(A[j-1].key>A[j].key) {
				swap(A[j-1],A[j]);
				flag = true; // flag若为true,则证明有逆序
			} 
		if(flag==false)	return;// flag若为false,则证明没有任何一对序列为逆序,即已经排好了 
	}
}

5. 快速(交换)排序

在这里插入图片描述
在这里插入图片描述

int Partition(int A[],int low,int high) {
	int pivot = A[low];
	while(low<high) {
		while(low<high&&A[high]>=pivot) 	
			high--;
		A[low]=A[high];
		while(low<high&&A[low]<=pivot)
			low++;
		A[high]=A[high];
	}
	A[low]=pivot;
	return low;
}

void QuickSort(int A[],int low,int high) {
	if(low<high) {
		int pivotpos = Partition(A,low,high);
		QuickSort(A,low,pivotpos-1);
		QuickSort(A,pivotpos+1,high);
	}
}

在这里插入图片描述
在这里插入图片描述

6. 直接(选择)排序

在这里插入图片描述

void SelectSort(int A[],int n) {
	for(int i=0;i<n-1;i++) {
		int min=i;
		for(int j=i+1;j<n;j++)
			if(A[j]<A[min])
				min=j;
		if(min!=i) swap(A[i],A[min]);
	}
}

在这里插入图片描述

七、树形结构

1、树的性质

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
例题
在这里插入图片描述

2、二叉树

二叉树的概念

在这里插入图片描述
例题

在这里插入图片描述
例题

在这里插入图片描述

满二叉树

在这里插入图片描述

完全二叉树

在这里插入图片描述

完全二叉树的性质

在这里插入图片描述

二叉排序树

在这里插入图片描述

平衡二叉树

在这里插入图片描述

二叉树的性质

在这里插入图片描述
(一式)结点的总数=度为0的结点数+度为1的结点数+度为2的结点数
(二式)结点的总数=度为0的结点,其孩子结点总数(为0)+度为1的结点,其孩子结点总数(为1n)+度为2的结点,其孩子结点总数(为2n)+根结点

联立两式得解,性质1

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

二叉树的存储结构

二叉树的顺序存储

完全二叉树的顺序存储

在这里插入图片描述
在这里插入图片描述

非完全二叉树的顺序存储

在这里插入图片描述
在这里插入图片描述

二叉树的链式存储

在这里插入图片描述

typedef struct BiTNode {
	int data;
	struct BiTNode *lchild,*rchild;
}BiTNode,*BiTNode;

在这里插入图片描述

二叉树的遍历

在这里插入图片描述

二叉树的先序遍历

//二叉树的先序遍历
void PreOrder(BiTNode T) {
	if(T!=NULL) {
		visit(T);
		PreOrder(T->lchild);
		PreOrder(T->rchild);
	}
}

在这里插入图片描述
在这里插入图片描述

二叉树的中序遍历

//二叉树的中序遍历
void InOrder(BiTNode T) {
	if(T!=NULL) {
		InOrder(T->lchild);
		visit(T);
		InOrder(T->rchild);
	}
}

在这里插入图片描述
在这里插入图片描述

二叉树的后序遍历

//二叉树的后序遍历
void PostOrder(BiTNode T) {
	if(T!=NULL) {
		PostOrder(T->lchild);
		PostOrder(T->rchild);
		visit(T);
	}
}

在这里插入图片描述
在这里插入图片描述

二叉树的中序遍历(非递归)

在这里插入图片描述

//二叉树的中序遍历(非递归)
void InOrder2(BiTNode T) {
	InitStack(S);
	BiTree p = T;
	while(p||!IsEmpty(S)) {
		if(p) {
			Push(S,p);
			p=p->lchild;
		} else {
			Pop(S,p);
			visit(p);
			p=p->rchild;
		}
	}
}

在这里插入图片描述

二叉树的层次遍历

在这里插入图片描述

//二叉树的层次遍历
void levelOrder(BiTNode T) {
	InitQueue(Q);
	BiTree p;
	EnQueue(Q,p);
	while(!isEmpty Q) {
		DeQueue(Q,p);
		visit(p);
		if(p->lchild!=NULL) EnQueue(Q,p->lchild);
		if(p->rchild!=NULL) EnQueue(Q,p->rchild);
	}
}

在这里插入图片描述

由遍历序列构造二叉树(重中之重)

在这里插入图片描述
例题

先在先序遍历序列里找到根结点(第一个元素),根结点(第一个元素)将中序遍历序列划分为两个部分(425,63),分别在左子树和右子树
在这里插入图片描述
然后在先序遍历序列里找到左子树根结点(第二个元素),根结点(第二个元素)将中序遍历序列又划分为两个部分(4,5),分别在左子树和右子树
在这里插入图片描述
再看右子树的部分,在先序遍历序列里找到右子树根结点(6,3)中先出现的元素,即(3),故右子树根结点就是(3),最后(6)在左子树
在这里插入图片描述

线索二叉树

先序线索化
先序线索化
中序线索化
在这里插入图片描述
后序线索化
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

//线索二叉树
typedef struct ThreadNode {
	int data;
	struct ThreadNode *lchild,*rchild;
	int ltag,rtag;
}ThreadNode,* ThreadTree;

//中序线索二叉树线索化
void InThread(ThreadTree &p,ThreadTree &pre) {
	if(p!=NULL) {
		InThread(p->lchild,pre);
		if(p->lchild==NULL) {
			p->lchild = pre;
			p->ltag = 1;
		}
		if(pre!=NULL&&pre->rchild==NULL) {
			pre->rchild = p;
			pre->rtag = 1;
		}
		pre=p;
		InThread(pre->rchild,pre);
	}
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
线索二叉树的遍历

//中序线索二叉树的遍历
//找前驱结点
ThreadNode *FirstNode(ThreadNode *p) {
	while(p->ltag==0) p=p->lchild;
	return p;
} 

//找后继结点
ThreadNode *NextNode(ThreadNode *p) {
	if(p->rchild==0) return FirstNode(p->rchild);
	else return p->rchild;
} 

在这里插入图片描述

3、树的存储结构

双亲表示法

在这里插入图片描述

#define MAX_TREE_SIZE 100

//双亲表示法 
typedef strct {
	int data;
	int parent;
}PTNode; 

typedef struct {
	PYNode nodes[MAX_TREE_SIZE];
	int n;
}PTree;

在这里插入图片描述

孩子表示法

在这里插入图片描述

//孩子表示法 
typedef struct {
	int child;			//孩子结点的下标 
	struct CNode *next;	//下一个结点的指针 
}CNode;

typedef struct {
	int data;			//每个结点存放的元素 
	struct CNode *child;//单链表的头指针 
}PNode;

typedef struct {
	PNode nodes[MAX_TREE_SIZE];
	int n;				//该颗树的结点数量 
}CTree;

在这里插入图片描述

孩子兄弟表示法

在这里插入图片描述

//(左)孩子(右)兄弟表示法(类似于二叉链表) 
typedef struct CSNode {
	int data;
	struct CSNode *firstchild, *nextsibling;
}CSNode,CSTree;

在这里插入图片描述
在这里插入图片描述

4、树、森林与二叉树的转换

树与二叉树的转换

例题:利用左孩子右兄弟来进行树、森林与二叉树的转换
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

森林与二叉树的转换

例题:利用左孩子右兄弟来进行树、森林与二叉树的转换
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

5、树的遍历

先根遍历

在这里插入图片描述

后根遍历

在这里插入图片描述

6、森林的遍历

先序遍历(类似于先根遍历)

在这里插入图片描述

中序遍历(类似于后根遍历)

在这里插入图片描述

遍历总结

在这里插入图片描述

7、并查集(重中之重)

在这里插入图片描述
例题:

用树表示10个子集s0-s9,即10个根结点,将它们用孩子表示法存储,即保存元素的数据域data,和一个保存双亲结点下标的伪指针域parent(均为-1)
在这里插入图片描述
若变为了3个子集,s0-s2,同理,则变为以下3棵树:根结点parent的绝对值的大小表示该棵树结点的个数
在这里插入图片描述
基本操作
在这里插入图片描述

//并查集
int UFSets[SIZE];//默认数据与数组下标一致

void Initial(int S[]) {
	//每个元素都初始化成一个子集
	for(int i=0;i<size;i++) {
		S[i]=-1; //(因为每个元素都为一个子集)将所有的双亲结点的下标都修改为1	
	} 
} 

int Find(int S[],int x) {  //传入并查集S,和查找元素x 
	while(S[x]>=0) x=S[x]; //如果是正数,说明不是根结点,继续循环查找 
	return x;
} 

void Union(int S[],int Root1,int Root2)  {	//传入并查集S和两个子集根结点的下标 
	S[Root2]=Root1;
} 

例题:Union操作

在这里插入图片描述

8、树与森林总结

在这里插入图片描述

9、二叉排序树

在这里插入图片描述

查找

在这里插入图片描述

//二叉排序(查找)树
//查找
BTSNode *BST_Search(BiTree T,int key,BSTNode *p) {
	p = NULL;
	while(T!=NULL&&key!=T->data) {
		p=T;	//修改双亲结点为当前结点 
		if(key<T->data) T=T->lchild;
		else T=T->rchild;
	}
	return T;
} 

插入

在这里插入图片描述

//插入
bool BST_Insert(BiTree &T,int k) {
	if(T==NULL) {	//半段二叉树是否为空,若为空,则申请一块区域 
		T=(BiTree)malloc(sizeof(BSTNode));
		T->key = k; //结点赋值为k 
		T->lchild = T->rchild = NULL; //左右孩子结点赋值为空 
		return true;
	}
	else if(k==T->key) return false; //该值等于根结点,不插入 
	else if(k<T->key) return BST_Insert(T->lchild,k); //若小于根结点的值,则插入到左孩子结点 
	else return BST_Insert(T->rchild,k);//若大于根结点的值,则插入到右孩子结点
}

构造二叉排序树

在这里插入图片描述

//构造二叉排序树
void Creat_BST(BiTree &T,int str[],int n) {
	//传入:要构造的树,所有要插入元素的数组,插入结点的数量
	T=NULL;
	int i=0;//计数器,计数插入结点的数量 
	while(i<n) {
		BST_Insert(T,str[i]);//循环调用插入函数(见上) 
		i++} 
} 

删除

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

查找

在这里插入图片描述

10、平衡二叉树

在这里插入图片描述
例题:高度为h最小平衡二叉树的节点数(解递归方程)
在这里插入图片描述

平衡二叉树的判断

在这里插入图片描述

//平衡二叉树的判断 
viod Judege_AVL(BiTree bt,int *balance,int *h) {
	//传入参数:根结点bt,平衡性,高度 (传入指针就可以在函数内部修改了) 
	int bl = 0;//左子树平衡性
	int br = 0;//右子树平衡性
	int hl = 0;//左子树高度 
	int hr = 0;//右子树高度 
	if(bt==NULL) {
		h=0;
		balance=1;
	}
	else if(bt->lchild==NULL&&bt->rchild==NULL) {
		h=1;
		balance=1;
	}
	else {
		Judege_AVL(bt->lchild,bl,hl);
		Judege_AVL(bt->rchild,br,hr);
		if(hl>hr) h=hl+1;
		else h=hr+1;
		if(abs(hl-hr)<2&&bl==1&&br==1) balance=1;
		else balance=0;
	}
}

平衡二叉树的插入

11、哈夫曼树

在这里插入图片描述
在这里插入图片描述

构造哈夫曼树

例题:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

哈夫曼树的性质

在这里插入图片描述

哈夫曼树的应用——哈夫曼编码

在这里插入图片描述
修改后:
在这里插入图片描述
例题:
在这里插入图片描述

八、图形结构与图论算法模型

1、图的存储结构

邻接矩阵法

在这里插入图片描述
例题:有向图的存储
在这里插入图片描述
例题:无向图的存储(对称矩阵,可以压缩存储)
在这里插入图片描述
在这里插入图片描述
例题:网的存储
在这里插入图片描述
在这里插入图片描述

#deine MaxVertexNum 100
//邻接矩阵法
typedef char VertexType;//结点类型 
typedef int EdgeType;	//边的类型 
typedef struct {
	VertexType Vex[MaxVertexNum];				//结点集 
	EdgeType Edge[MaxVertexNum][MaxVertexNum];	//边集
	int vexnum,arcnum;							//结点数与边数 
}MGraph; 

性质:

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

邻接表法

在这里插入图片描述
在这里插入图片描述
例题:有向图的邻接表法
在这里插入图片描述
例题:无向图的邻接表法
在这里插入图片描述

//邻接表法

//定义边表结点 
typedef struct ArcNode {
	int adjvex;				//下个顶点的数组下标 
	struct ArcNode *next;	//指向下一个链表结点的指针域 
	//若有权重,也可以加上权重 
}ArcNode;

//定义顶点表 
typedef struct VNode {
	VertexType data;		//该顶点的数据域 
	ArcNode *first;			//指向属于它边表的头指针 
}VNode,AdjList[MaxVertexNum]; //数组就是顶点表 

//定义邻接表
typedef struct {
	AdjList vetices;		//所有的顶点 
	int vexnum,arcnum;		//结点数与边数 
}ALGraph; 

特点:
在这里插入图片描述
在这里插入图片描述

十字链表法

在这里插入图片描述
改进邻接表——可以快速找到所有顶点的入边,得到十字链表
在这里插入图片描述
在这里插入图片描述

//十字链表法

//定义边表结点 
typedef struct ArcNode {
	int tailvex,headvex;			//尾域和头域(数组下标) 
	struct ArcNode *hlink, *tlink;	//出弧单链表和入弧单链表 
	//若有权重,也可以加上权重 
}ArcNode;

//定义顶点表结点 
typedef struct VNode {
	VertexType data;						//该顶点的数据域 
	ArcNode *firstin, *firstout;			//出弧单链表和入弧单链表的头指针 
}; 

//定义十字链表
typedef struct {
	VNode xlist[MaxVertexNum];				//数组就是顶点表 
	int vexnum,arcnum;						//结点数与边数 
}GLGraph; 

邻接多重表法

在这里插入图片描述
无向图的邻接表删除要遍历两次边表——改进成为邻接多重表
在这里插入图片描述
在这里插入图片描述

//邻接多重表 

//定义边结点
typedef struct ArcNode {
	int ivex ,jvex;					//两个端点 
	struct ArcNode *ilink, *jlink;	//第一个端点的边表和第二个端点的边表 
	//若有权重,也可以加上权重 
	//bool mark;					//作为标记 
};

//定义顶点表结点
typedef struct VNode {
	VertexType data;			//该顶点的数据域 
	ArcNode *firstedge;			//指向对应边表的头指针 
};

//定义邻接多重表
typedef struct {
	VNode adjlist[MaxVertexNum];			//数组就是顶点表 
	int vexnum,arcnum;						//结点数与边数 
}AMLGraph; 

2、图的基本操作与暴力搜索思想(最基础的思路)

图的遍历——广度优先搜索(树的层次遍历)

广度优先搜索BFS

在这里插入图片描述
因为图中存在“环”,所以不能完全按照树的层次遍历算法,这样“顶点5”就会被访问两次,需要借助队列和辅助标记数组
在这里插入图片描述

//广度优先搜索
bool visited[MAX_TREE_SIZE];	//初始化辅助数组为FALSE 
//访问不连通的结点 
void BFSTraverse(Graph G) {
	for(int i=0;i<G.vexnum;++i) visited[i]=FALSE;
	InitQueue(Q);
	for(int i;i<G.vexnum;i++) {
		if(!visit[i]) BFS(G,i);
	}
} 

void BFS(Graph G,int v) {	//传入图G和初始的顶点 
	visit(v);				//访问这个顶点 
	visited[v] = TRUE;		//辅助数组下标修改为TRUE,防止重复访问(改进树的层次遍历) 
	EnQueue(Q,v);			//将v顶点入队到队列Q中 
	while(!isEmpty(Q)) {	//循环到队列为空 
		DeQueue(Q,v);		//出队队首元素 
		for(w=FirstNeighbor(G,v);w>=0;w=NextNeighbor(G,v,w)) {
			if(!visited[w]) {
				visit[v];
				visit[w]=TRUE;
				EnQueue(Q,w);
			}
		}
	}
}

在这里插入图片描述
应用:利用广度优先搜索思想无权图单元最短路径
在这里插入图片描述
与深度优先搜素不同,BFS总是优先搜索离初始状态最近的状态。(二叉树的层序遍历)

DFS利用栈
BFS利用队列
在这里插入图片描述
例题1:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
应用: 广度优先生成树
在这里插入图片描述

图的遍历——深度优先搜索(树的先序遍历)

深度优先搜索DFS(回溯法)

在这里插入图片描述
在这里插入图片描述

//深度优先搜索
bool visited[MAX_TREE_SIZE];	//初始化辅助数组为FALSE 
//访问不连通的结点 
void DFSTraverse(Graph G) {
	for(int i=0;i<G.vexnum;++i) visit[i]=FALSE;
	for(int i=0;i<G.vexnum;++i) if(!visited[i]) DFS(G,i);	//类似于树的先序遍历,采用递归 
}

void DFS(Graph G,int v) {	//传入图G和初始的顶点 
	visit(v);				//访问这个顶点 
	visited[v]=TRUE;		//辅助数组下标修改为TRUE,防止重复访问(改进树的先序遍历) 
	for(w=FirstNeighbor(G,v);w>=0;w=NextNeighbor(G,v,w)) if(!visited[v]) DFS(G,w);
}

在这里插入图片描述
例题:深度优先生成树
在这里插入图片描述
例题:遍历与连通性
在这里插入图片描述
在这里插入图片描述
有向图中没有上述两个结论
在这里插入图片描述
从某个状态开始,不断地转移状态直到无法转移,然后回退到前一步的状态;继续重复上述过程

利用了栈

用递归实现更简单
在这里插入图片描述
例题1:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
例题2:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
例题:[蓝桥杯2015初赛]方程整数解

题目描述 方程: a^2 + b^2 + c^2 = 1000 这个方程有正整数解吗?有:a,b,c=6,8,30 就是一组解。 求出
a^2 + b^2 + c^2 = n(1<=n<=10000)的所有解,解要保证c>=b>=a>=1。

输入
存在多组测试数据,每组测试数据一行包含一个正整数n(1<=n<=10000)

输出
如果无解则输出"No Solution"。 如果存在多解,每组解输出1行,输出格式:a b c,以一个空格分隔

按照a从小到大的顺序输出,如果a相同则按照b从小到大的顺序输出,如果a,b都相同则按照c从小到大的顺序输出。

样例输入 Copy
4
1000

样例输出 Copy
No Solution
6 8 30
10 18 24

#include <iostream>
#include <cstdio>
#include <vector>
#include <cstring>
#include <math.h>
using namespace std;


int n,a,b,c;

void fun(){
	int flag = 0;
	for(a = 1; a < sqrt(n); a++){
		for(b = a; b < sqrt(n); b++){
			for(c = b; c < sqrt(n);c++){
				if(a*a+b*b+c*c==n){
					flag = 1;
					cout << a<<" "<<b<<" "<<c<<endl;
				}
			}
		}
	}
	if(flag == 0){
		cout << "No Solution" << endl;
	}
}
int main () {
	while(cin >> n){
		fun();
	}
    return 0;
}

例题:
[蓝桥杯2015初赛]牌型种数

题目描述 小明被劫持到X赌城,被迫与其他3人玩牌。 一副扑克牌(去掉大小王牌,共52张),均匀发给4个人,每个人13张。
这时,小明脑子里突然冒出一个问题: 如果不考虑花色,只考虑点数,也不考虑自己得到的牌的先后顺序
自己手里能拿到的初始牌型组合一共有多少种呢?

输出
请输出该整数,不要输出任何多余的内容或说明文字。

思路一:暴力(超时)
首先想到的就是暴力解决,简单粗暴,不费脑子,最容易想到,
方法就是从1~K 一共13张牌,13个循环即可。

#include <stdio.h>
int main ()
{
	int a[13];  //用来表示牌的种类 
	int num=0;  //用来存放总的种数 
	  
            for(a[0]=0; a[0]<=4; a[0]++)   //每种点数的牌被抽到的张数有五种:0,1,2,3,4 
            {
                for(a[1]=0; a[1]<=4; a[1]++)
                {
                    for(a[2]=0; a[2]<=4; a[2]++)
                    {
                        for(a[3]=0; a[3]<=4; a[3]++)
                        {                       	
                            for(a[4]=0; a[4]<=4; a[4]++)
                            {
                                for(a[5]=0; a[5]<=4; a[5]++)
                                {
                                    for(a[6]=0; a[6]<=4; a[6]++)
                                    {    
                                        for(a[7]=0; a[7]<=4; a[7]++)
                                        {
                                            for(a[8]=0; a[8]<=4; a[8]++)
                                            {
                                                for(a[9]=0; a[9]<=4; a[9]++)
                                                {
                                                    for(a[10]=0; a[10]<=4; a[10]++)
                                                    {
                                                        for(a[11]=0; a[11]<=4; a[11]++)
                                                        {
                                                            for(a[12]=0; a[12]<=4; a[12]++)
                                                            {
                                                                if(a[0]+a[1]+a[2]+a[3]+a[4]+a[5]+a[6]+a[7]+a[8]+a[9]+a[10]+a[11]+a[12]==13)
                                                                {
                                                                    num++;
                                                                }
                                                            }
                                                        }
                                                    }
                                                }
                                            }
                                        }
                                    }
                                }    
                            }
                        }
                    }
                }
            }
    printf("%d",num);
    return 0;
 } 

思路二:递归
然后就要考虑递归来写了。
递归函数要包含递归主题,递归出口(判断递归结束的条件)。

递归思想:由于不论顺序,所以假定从A开始选择个数(0~4)直到 K(13)。
具体思路见如下代码。

#include <stdio.h> 

int num=0;   //we表示种类数 

//n表示每次选择的dian点的牌的数量
//dian表示牌的点数
//sum为13,即牌每个人发13张  

void my(int n,int dian,int sum)
{

	sum+=n;	
	if(sum==13)
	{
		num++;
		sum=0;
		return;
	}
	else if(sum>13)
	{
		return;
	}
	if(dian==13)
	{
		return;
	}
	for(int i=0;i<=4;i++)
	{
		my(i,dian+1,sum); 
	}
	return;
}

int main()
{
	int sum=0;//牌数=13 
	int dian=0;//点数<=13 
	my(0,dian,sum);
	printf("%d",num);
	return 0;
} 

暴力搜索思想例题

[蓝桥杯2015初赛]奇妙的数字

题目描述 小明发现了一个奇妙的数字。它的平方和立方正好把0~9的10个数字每个用且只用了一次。你能猜出这个数字是多少吗?

输出
请输出该数字,不要输出任何多余的内容。

分析:

  1. 定义变量x2,表示数字的平方,x3表示数字的立方。

  2. 定义一个数组test[10],10元素分别代表(0-9)10个数出现的次数,然后取出x2和x3的每一位,用数组的10个元素来标识各个数出现的次数,每出现一次加1,最后再判断数组test的10个元素是不是1。如果为1,则说明满足题的要求,如果非1,则说明不满足题的要求。

  3. 没有时间和内存限制,就暴力循环吧,当然了代码效率越高肯定是越好的。

#include <iostream>  
#include <memory.h> 
const int N = 1001;  
  
bool solve(int a,int b)  
{  
    int test[10];  
    memset(test,0,sizeof(test));  
    while(a)  
    {  
        test[a%10]++;  
        a/=10;  
    }  
    while(b)  
    {  
        test[b%10]++;  
        b/=10;  
    }  
    bool flag = true;  
    for(int i=0;i<10;i++)  
    {  
        if(test[i] != 1)  
        {  
            flag = false;  
            break;  
        }  
    }  
    return flag;  
}  
  
int main()   
{  
	using std::cout;
	int x2,x3;
    for(int i=10;;i++)  
    {  
        int x2 = i*i;  
        int x3 = i*i*i;  
        if(solve(x2,x3))  
        {  
            cout<<i;  
            break;  
        }  
    }  
    return 0;  
}

2、图的应用

最小生成树

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
由贪心算法思想得最小生成树算法:
在这里插入图片描述

九、数学:极限方法、无穷级数

1. 0的故事

以0为起点——数组下标

序列、向量、数组及其相互转化的观点

数组的遍历

通常的解法

for循环

= 还是 =

以1为起点——线性表位序、正常思维方式

数组下标与线性表位序的换算公式

二维数组下标与矩阵位序的换算公式

以n为起点——等差数列、级数下标变化、从m到n有几个数字

无穷级数下标变化的总结与证明

以顺序表的算法为例,找出其几何直观

初始化

遍历

查找

增加

删除

2. 数学归纳法与证明、循环、递归

放缩法

相关推荐
©️2020 CSDN 皮肤主题: 游动-白 设计师:白松林 返回首页