1、大纲
数据结构、算法(理解)
线性表:顺序表(数组)、链表(单向链表、单向循环链表、双向链表、双向循环链表)、栈(顺序栈、链式栈)、队列(循环队列、链式队列)
树:特性、二叉树(性质、创建、遍历)
排序方法、查询方法(原理、思路)
2、数据结构
- C语言如何写程序,学数据结构是为了简洁、高效的写程序。
- 如果遇到一个实际问题,需要写代码实现相应功能,需要解决两个问题:
- 如何表达数据之间的逻辑关系以及怎么存储到计算机中?
数据结构构成:
- 数据
逻辑结构
存储结构
操作(数据的运算)
数据:不再是单纯的数字,而是类似集合的概念
结构:数据之间的关系
- 采用什么方式解决问题?
采用算法去解决
程序 = 数据结构+算法
问题 --> 数据结构+算法 = 程序 --> 问题
2.1 数据
即:数据>数据元素>数据项
数据:不再是单纯的数字,而是类似集合的概念
数据元素:是数据的基本单位,由若干个数据项组成
数据项:数据的最小单位,描述数据元素有用信息
数据元素又叫为节点
例如:
计算机处理的对象(数据)已不再是单纯的数值:
图书管理中的数据,如下表所列:
2.2 逻辑结构——三种关系
数据元素并不是孤立存在的,它们之间存在着某种关系(或联系、结构)。元素和元素之间的关系:
2.2.1.线性关系
逻辑结构:线性结构
特点:一对一
线性结构:顺序表、链表、栈、队列
2.2.2.层次关系
逻辑结构:树形结构
特点:一对多
树形结构:二叉树
2.2.3.网状关系
逻辑结构:图状结构
特点:多对多
图状结构:图
例题:
田径比赛的时间安排问题
2.3 存储结构
数据的逻辑结构在计算机中的具体实现
2.3.1 顺序存储
数组:连续存储
特点:内存连续、随机存取,每个元素占用空间较少
缺点:只能用一块大的且连续的空间,会产生一些碎片化的小空间
2.3.2 链式存储
通过指针存储
特点:内存不连续,通过指针实现
链表实现:
结构体:
#include <stdio.h>
struct node_t
{
int data; // 数据域:存放节点的数据
struct node_t *next;// 指针域:结构体指针指向下一个节点
};
int main(int argc, char const *argv[])
{
struct node_t A = {1, NULL};
struct node_t B = {2, NULL};
struct node_t C = {3, NULL};
A.next = &B;
B.next = &C;
return 0;
}
2.3.3 索引存储结构
在存储数据的同时,建立一个附加的索引表。
即索引存储结构=索引表+数据文件。
可以提高查找速度,特点检索速度快,但是占用内存多,删除数据文件要及时更改索引表。
例如:
这样查找一个电话就可以先查找索引表,再查找对应的数据文件,加快了查询的速度。但是如果删除或添加某个数据也要操作对应的索引表。
2.3.4 散列存储
数据存储按照和关键码之间的关系进行存取。关系由自己决定,比如关键码是key, 存储位置也就是关系是key+1。获取关键数据,通过元素的关键码方法的返回值来获取。
存的时候按关系存
取的时候按关系取
2.4.操作
本质:增 删 改 查
概念:算法是解决问题的思想方法,数据结构是算法的基础。
2.5.算法
2.5.1. 算法的设计
算法的设计:取决于数据的逻辑结构
算法的实现:依赖于数据的存储结构
2.5.2. 特性
有穷性:步骤是有限
确定性:每一个步骤有明确的含义,无二义性
可行性:规定的时间能完成
输入
输出
2.5.3.评价算法的好坏
- 正确性
- 易读性
- 健壮性:容错处理
- 高效性:执行效率,通过重复执行的次数来判断,也就是可以通过时间复杂度
2.5.4.时间复杂度:
语句频度:用时间规模函数表达
时间规模函数:T(n) = O(f(n))
T(n) // 时间规模函数的时间函数
O // 时间数量级
n // 问题规模 例:a[100], n = 100
f(n) // 算法可执行语句重复执行次数
例子1:
求1+2+3+4+...+n的和
算法1:
int sum=0;
for(int i=1;i<=n;i++)
{
sum+=i;
}
// n = 100
f(n) = n;
T(n) = O(n);
算法2:
利用等差数列前n项和公式:Sn=n(a1+an)/2
int sum = n*(n+1)/2 // 当 n=100 重复执行一次
f(n) = 1;
O(f(n));
T(n) = O(1);
例2:
int i, j;
for(i = 0; i< n; i++)
{
for(j = 0; j< n; j++)
{
printf("ok\n");
}
}
// n*n 次
T(n) = O(n^2)
例3:
int i, j;
for(i = 0; i< n; i++)
{
for(j = 0; j<=i; j++)
{
printf("ok\n");
}
}
执行次数:1+2+3+..+n
f(n) = n*(n+1)/2;
= n^2/2+n/2; // 只保留最高项n^2/2,除以最高项系数 得到n^2
T(n) = O(n^2)
计算大O的方法
- 根据问题规模n写出表达式f(n)
- 如果有常数项,将其置为1 //当f(n)的表达式中只有常数项的时候,例如f(n)=8 ==> O(1)
- 只保留最高项,其他项舍去。
- 如果最高项系数不为1,则除以最高项系数。
f(n) = 3*n^4 + 2*n^3 + 6*n^7 +10;
==> O(n^7)
- 线性表
线性表是最基本、最简单、也是最常用的一种数据结构,可以存储逻辑关系为线性的数据。线性表(linear list)是数据结构的一种,一个线性表是n个具有相同特性的数据元素的有限序列。
包含:顺序表(数组)、链表(单向链表、单向循环链表、双向链表、双向循环链表)、栈(顺序栈、链式栈)、队列(循环队列、链式队列)
逻辑结构:线性结构
存储结构:顺序存储(通过数组)或链式存储(通过指针)
特点:一对一,每个节点最多一个前驱和一个后继,首节点无前驱,尾节点无后继。
3. 顺序表
顺序表存储数据的具体实现方案是:将数据全部存储到一整块内存空间中,数据元素之间按照次序挨个存放。
举个简单的例子,将 {1,2,3,4,5} 这些数据使用顺序表存储,数据最终的存储状态如下图所示:
3.1.1 顺序表的特性
逻辑结构:线性结构
存储结构:顺序存储
操作:增删改查
特点:
- 顺序表的内存空间连续。
- 可以查找元素,高效的按下标进行操作。
- 在顺序表中间插入或删除元素时都涉及到元素的移动,效率较低。
- 顺序表长度固定
顺序表总结:
- 内存连续存储
- 长度固定
- 查找、修改比较容易,删除和插入麻烦。
3.1.2 操作数组
例题:
int a[100] = {1, 2, 3, 4, 5, 6, 7, 8};
函数命令规则:
下划线法:create_empty_seqlist
小驼峰法:createEmptySeqList
大驼峰法:CreateEmptySeqList
- 插入数组元素
/*
功能:向数组的第几个位置插数据
函数:void insetIntoA(int *p,int n, int post, int data);
参数:
int *p: 保存数组首地址
int n: 有效数据元素的个数
int post: 插入元素下标
int data: 数据
*/
void insertIntoA(int *p, int n, int post, int data)
{
// 1. 把从最后一个元素p[n-1]到插入位置元素 p[post]向后移动一个位置
for(int i = n-1; i >= post; i--)
p[i+1] = p[i];
// 2. 插入新元素data到指定位置
p[post] = data;
}
- 遍历数组中的有效元素
/*
功能:遍历数组中的有效元素
函数:void showA(int *p,int n);
参数:
int *p:保存数组收地址
int n:有效数据元素的个数
*/
void showA(int *p,int n)
{
for(int i = 0; i < n; i++)
printf("%d ", p[i]);
printf("\n");
}
- 删除数组元素
/* 功能:删除数组中指定元素
函数:void deleteIntoA(int *p,int n, int post);
参数:
int *p: 保存数组首地址
int n: 有效数据元素的个数
int post: 删除元素下标
*/
void deleteIntoA(int *p, int n, int post)
{
// 从删除位置后一个元素 p[post+1]到最后一个元素 p[n-1]往前移动一个单位
for(int i = post+1; i < n; i++)
p[i-1] = p[i];
}
#include <stdio.h>
void insertIntoA(int *p, int n, int post, int data)
{
// 1. 把从最后一个元素p[n-1]到插入位置元素 p[post]向后移动一个位置
for(int i = n-1; i >= post; i--)
p[i+1] = p[i];
// 2. 插入新元素data到指定位置
p[post] = data;
}
void showA(int *p,int n)
{
for(int i = 0; i < n; i++)
printf("%d ", p[i]);
printf("\n");
}
void deleteIntoA(int *p, int n, int post)
{
// 从删除位置后一个元素 p[post+1]到最后一个元素 p[n-1]往前移动一个单位
for(int i = post+1; i < n; i++)
p[i-1] = p[i];
}
int main(int argc, char const *argv[])
{
int arr[100] = {1, 2, 3, 4, 5, 6, 7, 8};
insertIntoA(arr, 8, 2, 100);
showA(arr, 9);
deleteIntoA(arr, 9, 3);
showA(arr, 8);
return 0;
}
- 添加全局变量last 表示有效元素下标
#include <stdio.h>
int last = 7; // 代表最后一个有效元素的下标 last=有效元素个数-1
void insertIntoA(int *p, int post, int data)
{
// 1. 把从最后一个元素p[n-1]到插入位置元素 p[post]向后移动一个位置
for(int i = last; i >= post; i--)
p[i+1] = p[i];
// 2. 插入新元素data到指定位置
p[post] = data;
last++;
}
void showA(int *p)
{
for(int i = 0; i <= last; i++)
printf("%d ", p[i]);
printf("\n");
}
void deleteIntoA(int *p, int post)
{
// 从删除位置后一个元素 p[post+1]到最后一个元素 p[n-1]往前移动一个单位
for(int i = post+1; i <=last; i++)
p[i-1] = p[i];
last--;
}
int main(int argc, char const *argv[])
{
int arr[100] = {1, 2, 3, 4, 5, 6, 7, 8};
insertIntoA(arr, 2, 100);
showA(arr);
deleteIntoA(arr, 3);
showA(arr);
return 0;
}
3.1.3 顺序表编程实现
seqlist.h
#ifndef __SEQLIST_H__
#define __SEQLIST_H__
#define N 10
typedef struct seqlist
{
int data[N];
int last; //代表数组中最后一个有效元素的下标
} seqlist_t;
//1.创建一个空的顺序表
seqlist_t *CreateEpSeqlist();
//2.向顺序表的指定位置插入数据
int InsertIntoSeqlist(seqlist_t *p, int post, int data);
//3.遍历顺序表sequence顺序list表
void ShowSeqlist(seqlist_t *p);
//4.判断顺序表是否为满,满返回1,未满返回0
int IsFullSeqlist(seqlist_t *p);
//5.判断顺序表是否为空
int IsEpSeqlist(seqlist_t *p);
//6.删除顺序表中制定位置的数据
int DeleteIntoSeqlist(seqlist_t *p, int post);
//7.清空顺序表 (清空:访问不到,但是内存中还有;销毁:内存清空)
void ClearSeqList(seqlist_t *p);
//8.修改指定位置的数据,post为被修改数据位置,data为修改成的数据
int ChangePostSeqList(seqlist_t *p,int post,int data);
//9.查找制定数据出现位置,data为被查找的数据,返回下标,未找到返回-1
int SearchDataSeqList(seqlist_t *p,int data);
#endif
#include <stdio.h>
#include <stdlib.h>
#include "seqlist.h"
//1.创建一个空的顺序表
seqlist_t *CreateEpSeqlist()
{
/*1. 动态申请一块空间存放顺序表 */
seqlist_t *p = (seqlist_t *)malloc(sizeof(seqlist_t));
if (p == NULL)
{
perror("malloc last!");
return NULL;
}
/*2. 对last初始化 */
p->last = -1;
return p;
}
//4.判断顺序表是否为满,满返回1,未满返回0
int IsFullSeqlist(seqlist_t *p)
{
return p->last+1 == N;
}
//2.向顺序表的指定位置插入数据
int InsertIntoSeqlist(seqlist_t *p, int post, int data)
{
// 1. 容错判断
if(IsFullSeqlist(p) || post < 0 || post > p->last+1)
{
perror("InsertIntoSeqlist err");
return -1;
}
// 2. 从下标为post到最后一个下标为last的元素向后移动一个单位
int i;
for (i = p->last; i >= post; i--)
p->data[i+1] = p->data[i];
p->data[post] = data;
p->last++;
return 0;
}
//3.遍历顺序表sequence顺序list表
void ShowSeqlist(seqlist_t *p)
{
for(int i = 0; i <= p->last; i++)
printf("%d ", p->data[i]);
printf("\n");
}
//5.判断顺序表是否为空
int IsEpSeqlist(seqlist_t *p)
{
return p->last == -1;
}
//6.删除顺序表中指定位置的数据
int DeleteIntoSeqlist(seqlist_t *p, int post)
{
// 1. 容错判断
if(IsEpSeqlist(p) || post < 0 || post > p->last)
{
perror("DeleteIntoSeqlist err");
return -1;
}
// 2. 从下标为post+1到最后下标为last的元素向前移动一个单位
for (int i = post+1; i <= p->last; i++)
{
p->data[i-1] = p->data[i];
}
p->last--;
return 0;
}
//7.清空顺序表 (清空:访问不到,但是内存中还有;销毁:内存清空)
void ClearSeqList(seqlist_t *p)
{
p->last = -1;
}
//8.修改指定位置的数据,post为被修改数据位置,data为修改成的数据
int ChangePostSeqList(seqlist_t *p,int post,int data)
{
// 1. 容错判断
if(IsEpSeqlist(p) || post < 0 || post > p->last)
{
perror("ChangePostSeqList err");
return -1;
}
// 2. 修改指定位置的数据
p->data[post] = data;
return 0;
}
//9.查找制定数据出现位置,data为被查找的数据,返回下标,未找到返回-1
int SearchDataSeqList(seqlist_t *p,int data)
{
for (int i = 0; i <= p->last; i++)
{
if(p->data[i] == data)
return i;
}
return -1;
}
3链表 Link
链表又称单链表、链式存储结构,用于存储逻辑关系为“一对一”的数据。
和顺序表不同同,使用链表存储数据,不强制要求数据在内存中集中存储,各个元素可以分散存储在内存中。
所以在链表中,每个数据元素可以配有一个指针用于找到下一个元素即节点,这意味着,链表上的每个“元素”都长下图这个样子:
3.1 链表的特性
逻辑结构:线性结构
存储结构:链式存储结构
特点:内存中不连续,通过指针链接
解决问题:长度固定问题和插入删除麻烦问题
操作:增删改查
struct node_t
{
int data; // 数据域
struct node_t *next; // 指针域,指向下一个节点,存放的是下一个节点的地址
}
3.2 单向链表
- 有头单向链表:存在头节点,头节点指针域有效,数据域无效
- 无头单向链表:每一个数据域和指针域都有效
3.2 .1.遍历无头单向链表
#include <stdio.h>
typedef struct node
{
char data;
struct node *next;
} link_node_t, *link_list_t;
// 结构体类型
// struct node A; 等同 link_node_t A
// 结构体指针
// struct node * 等同 link_list_t
// struct node *p = &A; 等同 link_list_t p = &A;
int main(int argc, char const *argv[])
{
// 1. 定义3个节点
link_node_t A = {'a', NULL};
link_node_t B = {'b', NULL};
link_node_t C = {'c', NULL};
// 2. 将3个节点链接起来
A.next = &B;
B.next = &C;
// 3. 定义一个头指针,指向第一个节点,用于遍历链表
link_list_t h = &A;
// 4. 遍历无头链表
while (h != NULL)
{
printf("%c ", h->data);
h = h->next;
}
printf("\n");
return 0;
}
3.2 .2.有头单向链表的函数操作
头文件 linklist.h
#ifndef __LINKLIST_H__
#define __LINKLIST_H__
typedef int datatype;
typedef struct node_t
{
datatype data;//数据域
struct node_t *next;//指针域,指向自身结构体的指针
}link_node_t,*link_list_t;
//1.创建一个空的有头单向链表
link_node_t *createEmptyLinkList();
//2.链表指定位置插入数据
int insertIntoPostLinkList(link_node_t *p,int post, datatype data);
//3.计算链表的长度。
int lengthLinkList(link_node_t *p);
//4.遍历链表
void showLinkList(link_node_t *p);
//5.链表指定位置删除数据
int deletePostLinkList(link_node_t *p, int post);
//6.判断链表是否为空
int isEmptyLinkList(link_node_t *p);
//7.清空单向链表
void clearLinkList(link_node_t *p);
//8.修改指定位置的数据 post 被修改的位置 data修改成的数据
int changePostLinkList(link_node_t *p, int post, datatype data);
//9.查找指定数据出现的位置 data被查找的数据 //search 查找
int searchDataLinkList(link_node_t *p, datatype data);
//10.删除单向链表中出现的指定数据,data代表将单向链表中出现的所有data数据删除
int deleteDataLinkList(link_node_t *p, datatype data);
//11.转置链表
//解题思想:
//(1) 将头节点与当前链表断开,断开前保存下头节点的下一个节点,保证后面链表能找得到,定义一个q保存头节点的下一个节点,断开后前面相当于一个空的链表,后面是一个无头的单向链表
//(2) 遍历无头链表的所有节点,将每一个节点当做新节点插入空链表头节点的下一个节点(每次插入的头节点的下一个节点位置)
void reverseLinkList(link_node_t *p);
#endif
3.2.2.1.遍历链表
// 4.遍历链表
void showLinkList(link_node_t *p)
{
while (p->next != NULL)
{
p = p->next;
printf("%d ", p->data);
}
printf("\n");
}
3.2.2 .2.链表指定位置删除数据
//5.链表指定位置删除数据 post 代表的是删除的位置
int deletePostLinkList(link_node_t *p, int post)
{
link_list_t pdel = NULL;
// 1. 容错判断
if(post < 0 || post >= lengthLinkList(p) || isEmptyLinkList(p))
{
return -1;
}
// 2. 将头指针指向被删除位置的前一个节点
for (int i = 0; i < post; i++)
p=p->next;
// 3. 进行删除操作
// 1) 定义一个指针 pdel 指向被删除位置
pdel = p->next;
// 2) 跨过被删除节点
p->next = pdel->next;
// 3) 释放被删除节点
free(pdel);
pdel = NULL;
return 0;
}
3.2.2.3.判断链表是否为空
//6.判断链表是否为空
int isEmptyLinkList(link_node_t *p)
{
return p->next == NULL;
}
3.2.2.4.清空单向链表
思想:
循环进行删除,每次删除的头节点的下一个节点:
(1)定义一个pdel,指向被删除节点
(2)跨过被删除节点
(3)释放被删除节点
//7.清空单向链表
void clearLinkList(link_node_t *p)
{
link_list_t pdel = NULL;
while (p->next != NULL)
{
// 1. 每次删除的都是头节点的下一个节点
pdel = p->next;
// 2. 跨过被删除的节点
p->next = pdel->next;
// 3. 释放删除的节点
free(pdel);
pdel = NULL;
}
}
3.2.2.5.修改链表指定位置的数据
//8.修改指定位置的数据 post 被修改的位置 data修改成的数据
int changePostLinkList(link_node_t *p, int post, datatype data)
{
// 1. 容错判断
if(post < 0 || post >= lengthLinkList(p))
{
return -1;
}
// 2. 将头指针移动到修改节点的位置
for (int i = 0; i <= post; i++)
p = p->next;
// 3. 修改数据
p->data = data;
}
3.2.2.6.查询指定数据在链表的位置
//9.查找指定数据出现的位置 data被查找的数据 //search 查找
int searchDataLinkList(link_node_t *p, datatype data)
{
int post = 0; // 记录查找的位置
// 遍历有头链表
while (p->next != NULL)
{
p = p->next;
if(p->data == data)
return post;
post++;
}
return -1;
}
3.2.2.7.删除单向链表中出现的指定数据,data代表将单向链表中出现的所有data数据删除。
思想:p始终指向被删除节点的前一个,让q相当于遍历无头节点,pdel用于指向删除节点
//9.查找指定数据出现的位置 data被查找的数据 //search 查找
int searchDataLinkList(link_node_t *p, datatype data)
{
int post = 0; // 记录查找的位置
// 遍历有头链表
while (p->next != NULL)
{
p = p->next;
if(p->data == data)
return post;
post++;
}
return -1;
}
3.2.2.8.删除单向链表中出现的指定数据,data代表将单向链表中出现的所有data数据删除。
思想:p始终指向被删除节点的前一个,让q相当于遍历无头节点,pdel用于指向删除节点。
//10.删除单向链表中出现的指定数据,data代表将单向链表中出现的所有data数据删除
int deleteDataLinkList(link_node_t *p, datatype data)
{
link_list_t pdel = NULL; // 用于指向被删除节点
// 1. 定义一个指针q,指向头节点的下一个节点,那么其实又可以将q看做无头链表的头指针
link_list_t q = p->next;
// 2. 用q来遍历无头链表,将每一个节点的数据域与data做比较,如果相同就删除
while(q != NULL)
{
if (q->data == data)
{
// 1) 将pdel指向被删除的节点
pdel = q;
// 2) 跨过被删除节点
p->next = pdel->next;
// 3) 释放被删除节点
free(pdel);
pdel = NULL;
// 4) 将q指向链表删除后节点的下一个节点
q = p->next;
}
else
{
// 将头指针向后移动一个位置
p = p->next;
// 始终保证p指向q的前一个节点
q = p->next;
}
}
return 0;
}
3.2.2.9.转置链表
解题思想:
- 将头节点与当前链表断开,断开前保存下头节点的下一个节点,保证后面链表能找得到,定义一个q保存头节点的下一个节点,断开后前面相当于一个空的链表,后面是一个无头的单向链表。
- 遍历无头链表的所有节点,将每一个节点当做新节点插入空链表头节点的下一个节点(每次插入的头节点的下一个节点位置)。
void reverseLinkList(link_node_t *p)
{
link_list_t temp = NULL;
// 1.断开前,保存头节点的下一个节点的地址,用作遍历无头链表
link_list_t q = p->next; // 此时q相当于无头链表的头指针
// 2. 断开链表
p->next = NULL;
// 3. 遍历无头链表
while(q != NULL)
{
// 1) 提前将q的下一个节点用temp保存起来
temp = q->next;
// 2) 先连后面,在连前面,将无头链表的节点插入头节点的下一个位置
q->next = p->next;
p->next = q;//执行完这样代码后,q无法再找到下一个节点,
//所以需要在此行执行之前将q的下一个节点的地址提前保存起来
// 2) 将q移动,指向无头链表的下一个节点
q = temp;
}
}
3.3 单向循环链表
约瑟夫环问题,是一个经典的循环链表问题,题意是:已知 n 个人(分别用编号 1,2,3,…,n 表示)围坐在一张圆桌周围,从编号为 k 的人开始顺时针报数,数到 m 的那个人出列;他的下一个人又从 1 开始,还是顺时针开始报数,数到 m 的那个人又出列;依次重复下去,直到圆桌上剩余一个人。
用解决约瑟夫环问题进行杀猴子:
思想:用头指针移动到要杀的猴子的前一个,然后跨过指向猴子的节点。
circlelinklist.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
typedef struct node_t
{
int data;
struct node_t *next;
}link_node_t,*link_list_t;
int main(int argc, const char *argv[])
{
int i;
link_list_t pdel = NULL;//用于指向被删除节点
link_list_t ptail = NULL;//永远指向当前链表的尾
link_list_t pnew = NULL;//永远指向新创建的节点
link_list_t h = NULL;
int all_num = 7;//猴子总数
int start_num = 2; //从几号猴子开始数
int kill_num = 3;//数到几杀死猴
printf("请您输入猴子总数 起始号码 数到几杀死:\n");
scanf("%d%d%d",&all_num,&start_num,&kill_num);
//1.创建出一个单向循环链表
//(1)创建有all_num个节点的单向链表
h = (link_list_t)malloc(sizeof(link_node_t));
if(NULL == h)
{
perror("malloc failed");
return -1;
}
h->data = 1;
h->next = NULL;
ptail = h;//尾指针指向当前的第一个节点
for(i = 2; i <= all_num; i++)
{
//创建新的节点
pnew = (link_list_t)malloc(sizeof(link_node_t));
if(NULL == pnew)
{
perror("malloc failed");
return -1;
}
//将新节点装上数据
pnew->data = i;
pnew->next = NULL;
//将新节点链接到链表尾
ptail->next = pnew;//链接到链表的尾
ptail = pnew;//尾指针继续指向当前链表的尾
}
//(2)将头指针保存到链表的尾形成单向循环链表
ptail->next = h;//形成单向循环链表
#if 0 //用于调试程序
while(1)
{
printf("%d\n",h->data);
h = h->next;
sleep(1);
}
#endif
//2.开始杀猴子
//(1)将头指针移动到开始猴子的号码处
for(i = 1; i < start_num; i++)
h = h->next;
printf("start :%d\n",h->data);
//(2)循环进行杀猴子
while(h != h->next)//终止:就剩一个猴子,只有一个节点
{
//将头指针移动到即将删除节点的前一个节点
for(i = 1; i < kill_num-1; i++)
h = h->next;
pdel = h->next;
//跨过删除节点
h->next = pdel->next;
printf("kill is -------------%d\n",pdel->data);
free(pdel);
pdel = NULL;
//杀死猴子猴后,从下一个节点开始继续开始数,将头指针移动到开始数的地方
h = h->next;
}
printf("king is=================== %d\n",h->data);
return 0;
}
总结:顺序表和单向链表比较
- 顺序表在内存当中连续存储的(数组),但是链表在内存当中是不连续存储的,通过指针将数据链接在一起。
- 顺序表的长度是固定的,但是链表长度不固定
- 顺序表查找方便,但是插入和删除效率低,链表:插入和删除方便,查找效率低。
4.栈 Stack
4.1 什么是栈
- 元素进栈和出栈的操作只能从一端完成,另一端是封闭的
- 栈中无论存数据还是取数据,都必须遵循“先进后出”的原则,即最先入栈的元素最后出栈。以图 1 的栈为例,很容易可以看出是元素 1 最先入栈,然后依次是元素 2、3、4 入栈。在此基础上,如果想取出元素 1,根据“先进后出”的原则,必须先依次将元素 4、3、2 出栈,最后才能轮到元素 1 出栈。
栈是只能在一端进行插入和删除操作的线性表(又称为堆栈),进行插入和删除操作的一端称为栈顶,另一端称为栈底。
特点:栈是先进后出FILO(First In Last Out)
后进先出 LIFO(Last In First Out)
4.2 顺序栈
4.2.1 特性
逻辑结构:线性结构
存储结构:顺序结构
操作:创建、入栈、出栈、清空、判断空和满
4.2.2 代码实现
头文件seqstack.h:
#ifndef __SEQSTACK_H__
#define __SEQSTACK_H__
typedef int datatype;
typedef struct seqstack
{
datatype *data; //指向栈的存储位置
int maxlen; //保存栈的最大长度
int top; //称为栈针,用的时候可以当作顺序表里的last来使用
//top始终代表当前栈内最后一个有效元素的下标
} seqstack_t;
//1.创建一个空栈,len代表创建栈时的最大长度。
seqstack_t *createEmptySeqStack(int len);
//2.判断是否为满,满返回1 未满返回0
int isFullSeqStack(seqstack_t *p);
//3.入栈,data代表入栈的数据
int pushStack(seqstack_t *p, int data);
//4.判断栈是否为空
int isEmptySeqStack(seqstack_t *p);
//5.出栈,返回出栈数据
int popSeqStack(seqstack_t *p);
//6. 清空栈
void clearSeqStack(seqstack_t *p);
//7. 获取栈顶数据(注意不是出栈操作,如果出栈,相当于删除了栈顶数据,只是将栈顶的数据获取到,不需要移动栈针)
int getTopSeqStack(seqstack_t *p);
//8. 求栈的长度,返回长度。
int lengthSeqStack(seqstack_t *p);
#endif
- 创建一个空栈
//1.创建一个空栈,len代表创建栈时的最大长度。
seqstack_t *createEmptySeqStack(int len)
{
// 1. 申请空间存放栈的结构体
seqstack_t *p = (seqstack_t *)malloc(sizeof(seqstack_t));
// 2. 初始化
p->maxlen = len;
p->top = -1;
p->data = (datatype *)malloc(sizeof(datatype) * len);
return p;
}
- 判断是否为满,满返回1,未满返回0
//2.判断是否为满,满返回1 未满返回0
int isFullSeqStack(seqstack_t *p)
{
return p->top+1 == p->maxlen;
}
- 入栈
//3.入栈,data代表入栈的数据
int pushStack(seqstack_t *p, int data)
{
// 1. 容错判断
if(isFullSeqStack(p))
{
perror("pushStack: isFullSeqStack");
return -1;
}
// 2. 往上移动栈针
p->top++;
// 3. 将数据入栈
p->data[p->top] = data;
}
- 判断栈是否为空
//4.判断栈是否为空
int isEmptySeqStack(seqstack_t *p)
{
return p->top == -1;
}
- 出栈
//5.出栈,返回出栈数据
int popSeqStack(seqstack_t *p)
{
// int data = p->data[p->top];
// p->top--;
// return data;
// 1. 容错判断
if(isEmptySeqStack(p))
{
perror("popSeqStack: isEmptySeqStack");
return -1;
}
// 2. 往下移动栈针
p->top--;
// 3. 将栈顶数据取出
return p->data[p->top+1];
}
- 清空栈
//6. 清空栈
void clearSeqStack(seqstack_t *p)
{
p->top = -1;
}
- 获取栈顶数据
// 7. 获取栈顶数据(注意不是出栈操作,如果出栈,相当于删除了栈顶数据,只是将栈顶的数据获取到,不需要移动栈针)
int getTopSeqStack(seqstack_t *p)
{
// 1. 容错判断
if (isEmptySeqStack(p))
{
perror("getTopSeqStack: isEmptySeqStack");
return -1;
}
return p->data[p->top];
}
- 求栈的实际长度
//8. 求实际栈的长度,返回长度。
int lengthSeqStack(seqstack_t *p)
{
return p->top+1;
}
main函数:
int main(int argc, char const *argv[])
{
seqstack_t *p = createEmptySeqStack(5);
for (int i = 1; i <= 6; i++)
{
pushStack(p, i);
}
printf("栈顶是:%d\n", getTopSeqStack(p));
//只要栈不为空就出栈
printf("出栈");
while(!isEmptySeqStack(p))
{
printf("%d ", popSeqStack(p)); //出栈的顺序是 5 4 3 2 1 ,因为栈的思想先进后出
}
printf("\n");
return 0;
}
练习:
软通动力校园招聘笔试题
1. 若进栈顺序为 1,2,3,4 一下四种情况不可能出现的出栈序列是( )
A. 1,4,3,2
B. 2,3,4,1
C. 3,1,4,2
D. 3,4,2,1
C
- 下列叙述正确的是( )
A. 线性表是线性结构
B. 栈与队列是非线性结构
C. 线性链表是非线性结构
- 二叉树是线性结构
A
3. 下列关于栈叙述正确的是( )
A.在栈中只能插入数据
B.在栈中只能删除数据
C.栈是先进先出的线性表
D.栈是先进后出的线性表
D
- 请问下面的程序有问题吗?如果有问题在哪儿?
#include <stdio.h>
#include <stdlib.h>
void get_memory(int *q)
{
q = (int *)malloc(sizeof(int));
}
int main()
{
int i;
int *p = NULL;
get_memory(p);
for(i = 0; i < 10; i++)
{
p[i] = i;
}
for(i = 0; i < 10; i++)
{
printf("%d\n", p[i]);
}
return 0;
}
错误:相当于值传递,并且开辟的空间大小不够用
修改:可以通过传递二级指针,或者返回值。
#include <stdio.h>
#include <stdlib.h>
void get_memory(int **q)
{
*q = (int *)malloc(sizeof(int) * 10);
}
int main()
{
int i;
int *p = NULL;
get_memory(&p);
for(i = 0; i < 10; i++)
{
p[i] = i;
}
for(i = 0; i < 10; i++)
{
printf("%d\n", p[i]);
}
free(p);
p=NULL;
return 0;
}
4.3 链式栈
4.3.1 特性
逻辑结构:线性结构
存储结构:链式存储
顺序栈和链式栈的区别:存储结构不同,实现的方式不同,顺序栈用顺序表实现而链栈用链表实现。
栈的操作:创建、入栈、出栈、清空、获取
4.3.2 代码实现
头节点:
#ifndef __LINKSTACK_H__
#define __LINKSTACK_H__
typedef int datatype;
typedef struct linkstack
{
datatype data;
struct linkstack *next;
} linkstack_t;
//1.创建一个空的栈
void createEmptyLinkStack(linkstack_t **ptop);
//2.入栈,ptop是传入的栈针的地址,data是入栈的数据
int pushLinkStack(linkstack_t **ptop, datatype data);
//3.判断栈是否为空
int isEmptyLinkStack(linkstack_t *top);
//4.出栈
datatype popLinkStack(linkstack_t **ptop);
//5.清空栈
void clearLinkStack(linkstack_t **ptop);
//6.求栈的长度
int lengthLinkStack(linkstack_t *top);
//7.获取栈顶数据,不是出栈,不需要移动main函数中的top,所以用一级指针
datatype getTopLinkStack(linkstack_t *top);
#endif
-
- 创建一个空的栈
//1.创建一个空的栈
void createEmptyLinkStack(linkstack_t **ptop)
{
*ptop = NULL;
}
-
- 入栈,ptop是传入的栈针的地址,data是入栈的数
//2.入栈,ptop是传入的栈针的地址,data是入栈的数据
int pushLinkStack(linkstack_t **ptop, datatype data)
{
// 1. 创建一个新节点保存即将入栈的数据
linkstack_t *pnew = (linkstack_t *)malloc(sizeof(linkstack_t));
// 2. 申请节点空间就是为了data
pnew->data = data;
// 3. 将新节点插入无头链表的头
pnew->next = *ptop;
// 4. 移动栈针,栈针top永远指向无头单向链表的头
*ptop = pnew;
return 0;
}
-
- 判断栈是否为空
//3.判断栈是否为空
int isEmptyLinkStack(linkstack_t *top)
{
return top == NULL;
}
-
- 出栈
//4.出栈
datatype popLinkStack(linkstack_t **ptop)
{
linkstack_t *pdel = NULL;
// 1. 容错判断
if(isEmptyLinkStack(*ptop))
{
perror("isEmptyLinkStack!!!");
return -1;
}
// 2. 定义一个pdel指针,指向被删除的节点也就是栈顶的第一个节点
pdel = *ptop;
// 3. 定义一个临时变量,保存出栈的数据,也就是保存栈针所指节点中的数据域
datatype temp = (*ptop)->data;
// 4. 跨过删除节点,将栈针即头指针向后移动一个位置
(*ptop) = (*ptop)->next;
// 5. 释放被删除节点
free(pdel);
pdel = NULL;
// 6. 返回出栈数据
return temp;
}
-
- 清空栈
//5.清空栈
void clearLinkStack(linkstack_t **ptop)
{
while(!isEmptyLinkStack(*ptop))
popLinkStack(ptop);
}
-
- 求栈的长度
//6.求栈的长度
int lengthLinkStack(linkstack_t *top)
{
int len = 0;
while(top != NULL)
{
len++;
top = top->next;
}
return len;
}
-
- 获取栈顶数据
//7.获取栈顶数据,不是出栈,不需要移动main函数中的top,所以用一级指针
datatype getTopLinkStack(linkstack_t *top)
{
if(isEmptyLinkStack(top))
{
printf("getTopLinkStack: isEmptyLinkStack");
return -1;
}
return top->data;
}
主函数:
#include <stdio.h>
#include <stdlib.h>
#include "linkstack.h"
int main(int argc, const char *argv[])
{
int i;
//1,定义一个栈针 top
linkstack_t *top; //指向结构体节点的指针,此时top是野指针
createEmptyLinkStack(&top); //对指针变量top取地址,是二级指针
//执行之后,top被初始化为NULL,这里不用函数直接写top=NULL也可以,用函数更有条理。
for (i = 1; i <= 5; i++)
{
pushLinkStack(&top, i); //入栈的顺序 1 2 3 4 5
}
printf("len is %d\n", lengthLinkStack(top));
printf("top is %d\n", getTopLinkStack(top));
//clearLinkStack(&top);
//printf("清空栈之后:\n");
//printf("len is %d\n", lengthLinkStack(top));
while (!isEmptyLinkStack(top)) //出栈的顺序是 5 4 3 2 1
{
printf("%d ", popLinkStack(&top));
}
printf("\n");
return 0;
}
总结:
顺序栈和链式栈的区别是什么?
- 存储结构不同:顺序栈是顺序存储,内存连续;链式栈是链式存储,内存不连续。
- 顺序栈的长度受限制,而链栈不会。
5.1队列
5.1.1 特性
队列是只允许再两端进行插入和删除操作的线性表,在队尾插入,在队头删除,插入的一段被称为“队尾”,删除的一端被称为“队头”。队列包括顺序队列(循环队列)、链式队列。
结构:先进先出FIFO
操作:创建、入列、出列、判断是否为空、判断是否为满、清空。
注意:为了避免假溢出问题,即队列前面还有空闲,但是队尾已经出现越界,所以在实际使用队列时,为了使队列空间能重复使用,往往对队列的使用方法稍加改进,需要引入循环队列。一般顺序队列也指循环队列。
循环队列是把顺序队列首尾相连,把存储队列元素的表从逻辑上看成一个环,成为循环队列。
5.1.2.循环队列(顺序队列)
逻辑结构:线性结构
存储结构:顺序存储结构
操作:创建、入列、出列、判空和满、清空
#define N 6
typedef int datatype;
typedef struct
{
datatype data[N];//循环队列的数组
int rear;//存数据端 rear 后面
int front;//取数据端 front 前面
}sequeue_t;
利用"模运算" (大家习惯用这个方法)
front==(rear+1)%N;
代码:
#include <stdio.h>
#include <stdlib.h>
#define N 6
typedef int datatype;
typedef struct
{
datatype data[N];//循环队列的数组
int rear;//存数据端 rear 后面
int front;//取数据端 front 前面
}sequeue_t;
//循环队列中,假设数组的元素个数为N,那么循环队列中存储最多的数据个数为N-1个
// 创建一个空队列
sequeue_t *createEmptySequeue()
{
sequeue_t *p = (sequeue_t *)malloc(sizeof(sequeue_t));
if(p == NULL)
{
perror("createEmptySequeue malloc failed");
return NULL;
}
p->rear = 0; // 使用时是数组的下标 尾
p->front = 0; // 使用时是数组的下标 头
return p;
}
// 判断队列是否为满
int isFullSequeue(sequeue_t *p)
{
//思想上,舍去数组上的一个存储位置,用于判断队列是否为满
//先判断rear的下一个位置是否等于front
return (p->rear + 1) % N == p->front;
}
//入列 data代表入列的数据
int inSequeue(sequeue_t *p,datatype data)
{
// 1. 容错判断
if(isFullSequeue(p))
{
perror("isFullSequeue!!");
return -1;
}
// 2. 入列,对尾rear移动
p->data[p->rear] = data;
p->rear = (p->rear + 1) % N;
}
//判断队列是否为空
int isEmptySequeue(sequeue_t *p)
{
return p->rear == p->front;
}
//出列
datatype outSequeue(sequeue_t *p)
{
datatype temp; // 临时保存即将出列的数据
// 1. 容错判断
if(isEmptySequeue(p))
{
perror("isEmptySequeue!!!");
return -1;
}
// 2. 出列数据:思想是先取出数据,然后front向后移动
// 1) 取出数据
temp = p->data[p->front];
// 2) 移动对头 front
p->front = (p->front + 1) % N;
return temp;
}
//清空队列函数
void clearSequeue(sequeue_t *p)
{
// 思想,只要队列不为空,就出列
while(!isEmptySequeue(p))
outSequeue(p);
}
//求队列实际的长度
int lengthSequeue(sequeue_t *p)
{
// 长度情况分为 rear >= front
// rear < front
if(p->rear >= p->front)
{
return p->rear - p->front;
}
else
return p->rear + N - p->front;
}
int main(int argc, char const *argv[])
{
sequeue_t *p = createEmptySequeue();
for (int i = 1; i <= N-1; i++)
{
inSequeue(p, i);
}
printf("len is %d \n", lengthSequeue(p));
printf("rear is %d front is %d\n", p->rear, p->front);
outSequeue(p);
outSequeue(p);
printf("len is %d \n", lengthSequeue(p));
inSequeue(p, 100);
printf("len is %d \n", lengthSequeue(p));
printf("rear is %d front is %d\n", p->rear, p->front);
while(!isEmptySequeue(p))
printf("%d ", outSequeue(p));
printf("\n");
return 0;
}
循环队列,如果数组的元素个数为N,那么队列中最多能够存储的数据数的多少? N-1个 为什么?
答:rear 后面 队尾,在插入的时候,插入之前需要先判断 rear+1,也就是他的下一个为位置是否 等于 front 来判断队列是否为满,会造成浪费一个存储位置。
5.1.3. 链式队列
逻辑结构:线性结构
存储结构:链式结构
操作:创建、入列、出列、判空、清空
队列结构体:
typedef int datatype;
typedef struct node_t
{
datatype data;
struct node_t *next;
} linkqueue_node_t, *linkqueue_list_t;
typedef struct //将队列头指针和尾指针封装到一个结构体里
{
linkqueue_list_t front; //相当于队列的头指针
linkqueue_list_t rear; //相当于队列的尾指针
//有了链表的头指针和尾指针,那么我们就可以操作这个链表
} linkqueue_t;
建立空队列:
代码:
#include <stdio.h>
#include <stdlib.h>
typedef int datatype;
typedef struct node_t
{
datatype data;
struct node_t *next;
} linkqueue_node_t, *linkqueue_list_t;
typedef struct // 将队列头指针和尾指针封装到一个结构体里
{
linkqueue_list_t front; // 相当于队列的头指针
linkqueue_list_t rear; // 相当于队列的尾指针
// 有了链表的头指针和尾指针,那么我们就可以操作这个链表
} linkqueue_t;
// 1. 创建一个空的队列,用有头链表
linkqueue_t *createEmptyLinkQueue()
{
// 1) 申请队列空间,就是为了装东西
linkqueue_t *p = (linkqueue_t *)malloc(sizeof(linkqueue_t));
if (NULL == p)
{
perror("createEmptyLinkQueue p malloc err");
return NULL;
}
// 2) 申请链表的头节点空间,让rear和front都指向头结点
p->front = p->rear = (linkqueue_list_t)malloc(sizeof(linkqueue_node_t));
if (NULL == p->rear)
{
perror("createEmptyLinkQueue p->front malloc err");
return NULL;
}
// 可以将 p->front 当作h
// 可以将 p->rear 当作 ptail
p->rear->next = NULL;
return p;
}
// 2.入列 data代表入列的数据
int inLinkQueue(linkqueue_t *p, datatype data)
{
// 1.创建一个新的节点,用来保存即将插入的数据
linkqueue_list_t pnew = (linkqueue_list_t)malloc(sizeof(linkqueue_node_t));
// 2.将入列的数据放入到新的节点中
pnew->data = data;
pnew->next = NULL;
// 3.将新节点链链接到链表的尾巴
p->rear->next = pnew; // 新节点链接到链表的尾
p->rear = pnew; // rear移动,因为rear永远指向当前链表的尾
return 0;
}
// 4.判断队列是否为空
int isEmptyLinkQueue(linkqueue_t *p)
{
return p->rear == p->front;
}
// 3.出列
// 思想:每次释放front所指节点,然后移动front到后一个节点返回当前节点数据
datatype outLinkQueue(linkqueue_t *p)
{
linkqueue_list_t pdel = NULL; // 指向被删除的节点
datatype temp;
// 1. 容错判断
if (isEmptyLinkQueue(p))
{
perror("isEmptyLinkQueue!!");
return -1;
}
// 2. 出列数据
// 1) 定义pdel指向即将被删除的节点
pdel = p->front->next;
// 2) 跨过被删除的节点
p->front->next = pdel->next;
// 3) 将被删除节点的数据保存
temp = pdel->data;
if (p->front->next == NULL)
p->rear = p->front;
// 4) 释放节点
free(pdel);
pdel = NULL;
return temp;
}
// 5.求队列长度的函数
int lengthLinkQueue(linkqueue_t *p)
{
int len = 0;
linkqueue_list_t h = p->front;
while (h->next != NULL)
{
h = h->next;
len++;
}
return len;
}
// 6.清空队列
void clearLinkQueue(linkqueue_t *p)
{
while (!isEmptyLinkQueue(p))
outLinkQueue(p);
}
int main(int argc, char const *argv[])
{
linkqueue_t * p = createEmptyLinkQueue();
for (int i = 1; i <= 5; i++)
{
inLinkQueue(p,i);
}
printf("len is %d\n", lengthLinkQueue(p));
while (!isEmptyLinkQueue(p))
printf("%d ", outLinkQueue(p));
printf("\n");
inLinkQueue(p, 6);
printf("len is %d\n", lengthLinkQueue(p));
return 0;
}
6.双向链表
6.1. 特性
逻辑结构:线性结构
存储结构:链式结构
操作:增删改查
建立双向链表结构体:
//双向链表的节点定义
typedef int datatype;
typedef struct node_t
{
datatype data;//数据域
struct node_t *next;//指向下一个节点的指针 next 先前的
struct node_t *prior;//指向前一个节点的指针 prior 下一个
}link_node_t,*link_node_p;
//将双向链表的头指针和尾指针封装到一个结构体里
//思想上有点像学的链式队列
typedef struct doublelinklist
{
link_node_p head; //指向双向链表的头指针
link_node_p tail; //指向双向链表的尾指针
int len; //用来保存当前双向链表的长度
}double_list_t,*double_list_p;
6.2 双向链表相关操作
需要注意的是,与单链表不同,双链表创建过程中,每创建一个新节点都要与其前驱节点建立两次联系,分别是:
- 将新节点的 prior 指针指向直接前驱节点。
- 将直接前驱节点的 next 指针指向新节点。
- 创建空的双向链表
//1.创建一个空的双向链表
double_list_p createEmptyDoubleLinkList()
{
// 1. 申请空间存放头尾指针结构体
double_list_p p = (double_list_p)malloc(sizeof(double_list_t));
if(NULL == p)
{
perror("createEmptyDoubleLinkList");
return NULL;
}
p->len = 0;
//2.初始化,头尾指针分别指向开辟头节点,因为当前链表为空
p->head = p->tail = (link_node_p)malloc(sizeof(link_node_t));
if(p->head == NULL)
{
perror("p->head malloc failed!");
return NULL;
}
p->head->next = NULL;
p->head->prior = NULL;
return p;
}
- 指定位置插入
// 2.向双向链表的指定位置插入数据 post位置, data数据
int insertIntoDoubleLinkList(double_list_p p, int post, datatype data)
{
link_node_p temp = NULL; // 用来临时保存head或者tail的位置
if (post < 0 || post > p->len)
{
perror("insertIntoDoubleLinkList err");
return -1;
}
link_node_p pnew = (link_node_p)malloc(sizeof(link_node_t));
if (pnew == NULL)
{
perror("pnew malloc err");
return -1;
}
// 初始化新节点
pnew->data = data;
pnew->prior = NULL;
pnew->next = NULL;
// 插入链表的尾巴
if (post == p->len)
{
p->tail->next = pnew;
pnew->prior = p->tail;
p->tail = pnew; // 移动尾指针
}
// 中间差
else
{
if (post < p->len / 2) // 前半段
{
// temp移动到插入位置
temp = p->head;
for (int i = 0; i <= post; i++)
temp = temp->next;
}
else // 后半段
{
temp = p->tail;
for (int i = p->len - 1; i > post; i--)
temp = temp->prior;
}
// 2) 进行插入操作(先连前面,再练后面)
pnew->prior = temp->prior;
temp->prior->next = pnew;
pnew->next = temp;
temp->prior = pnew;
}
p->len++; // 插入完成,链表长度+1
return 0;
}
- 双向链表遍历
// 3.遍历双向链表
void showDoubleLinkList(double_list_p p)
{
link_node_p temp = NULL;
printf("正向遍历\n");
temp = p->head;
while (temp->next != NULL)
{
temp = temp->next;
printf("%d ", temp->data);
}
printf("\n");
printf("反向遍历\n");
temp = p->tail;
while (temp != p->head)
{
printf("%d ", temp->data);
temp = temp->prior;
}
printf("\n----------------\n");
}
- 判断双向链表是否为空
//4.判断双向链表是否为空
int isEmptyDoubleLinkList(double_list_p p)
{
return p->len == 0;
}
- 删除双向链表指定位置
//5.删除双向链表指定位置数据
int deletePostDoubleLinkList(double_list_p p,int post)
{
link_node_p temp = NULL;
// 1. 容错判断
if(isEmptyDoubleLinkList(p) || post >= p->len || post < 0)
{
error("deletePostDoubleLinkList err\n");
return -1;
}
//2.对删除位置进行分析,分为两种情况
if(post == p->len-1) // //删除的是链表最后一个节点
{
// 1)将尾指针向前移动一个位置
p->tail = p->tail->prior;
// 2)释放被删除节点,也就是最后一个节点
free(p->tail->next);
// 3)将最后一个节点与链表断开
p->tail->next = NULL;
}
else // 中间删除
{
if(post < p->len/2) // 说明在前半段
{
temp = p->head;
for (int i = 0; i <= post; i++)
temp = temp->next;
}
else // 说明在后半段
{
temp = p->tail;
for (int i = p->len-1; i > post; i--)
{
temp = temp->prior;
}
}
// 进行删除操作
temp->prior->next = temp->next;
temp->next->prior = temp->prior;
free(temp);
temp = NULL;
}
// 3. 双向链表的长度-1
p->len--;
return 0;
}
- 求双向链表的长度
//6.求双向链表的长度
int lengthDoubleLinkList(double_list_p p)
{
return p->len;
}
- 查找指定数据出现的位置
//7.查找指定数据出现的位置 data被查找的数据
int searchPostDoubleLinkList(double_list_p p,datatype data)
{
link_node_p temp = p->head;
int post =0; // 保存位置
while(temp->next != NULL)
{
temp = temp->next;
if(temp->data == data)
{
return post;
}
post++;
}
return -1;
}
- 修改指定位置的数据
//8.修改指定位置的数据,post修改的位置 data被修改的数据
int changeDataDoubleLinkList(double_list_p p,int post, datatype data)
{
if(isEmptyDoubleLinkList(p) || post >= p->len || post < 0)
{
perror("changeDataDoubleLinkList err");
return -1;
}
link_node_p temp = NULL;
if(post < p->len/2) // 说明在前半段
{
temp = p->head;
for (int i = 0; i <= post; i++)
temp = temp->next;
}
else
{
temp = p->tail;
for (int i = p->len-1; i > post; i--)
temp = temp->prior;
}
temp->data = data;
return 0;
}
- 删除双向链表中指定数据
// 9.删除双向链表中的指定数据 data代表删除所有出现的data数据
/*
思想:从头节点后节点开始用指针h遍历,相当于遍历无头链表,遇到需要删除节点的就用h指向它然后删除,如果不需要删除则h继续往后走一个。这里因为是双向链表可以找到前驱,所以不需要每次指向被删除节点的前一个然后跨过了。
*/
void deleteDataDoubleLinkList(double_list_p p, datatype data)
{
link_node_p h = p->head->next;
link_node_p pdel = NULL;
while (h != NULL) // 遍历双向链表(相当于遍历无头单向链表)
{
if (h->data == data) // 相等
{
if (h == p->tail) // 尾节点
{
p->tail = p->tail->prior;
free(p->tail->next);
p->tail->next = NULL;
h = NULL;
}
else // 中间节点
{
h->prior->next = h->next;
h->next->prior = h->prior;
pdel = h;
h = h->next;
free(pdel);
pdel = NULL;
}
p->len--;
}
else
{
h = h->next;
}
}
}
main函数代码:
int main(int argc, char const *argv[])
{
// int i;
double_list_p p = createEmptyDoubleLinkList();
for (int i = 0; i < 5; i++)
{
insertIntoDoubleLinkList(p, i, i);
}
showDoubleLinkList(p);
deletePostDoubleLinkList(p, 2);
printf("len is %d\n", lengthDoubleLinkList(p));
showDoubleLinkList(p);
deletePostDoubleLinkList(p, 3);
showDoubleLinkList(p);
printf("1 post is: %d\n", searchPostDoubleLinkList(p, 1));
int len = lengthDoubleLinkList(p);
for (int i = 0; i < len; i++)
{
deletePostDoubleLinkList(p, 0);
}
printf("len is %d\n", lengthDoubleLinkList(p));
showDoubleLinkList(p);
printf("is Empty? %d\n", isEmptyDoubleLinkList(p));
free(p);
p = NULL;
return 0;
}
作业:
- 整理思维导图
- 讲今天的代码敲两遍
- 定义一个函数,将其转换为二进制数,将所有二进制位存储到栈内,之后再出栈印输出
//我们打印的时候是不是先打印高位,后打印低位。
//求出一个二进制位之后,就入栈,当所有二进制位入栈之后,再统一出栈打印
num = 10; ----> 1010
10 % 2 = 0 //低位
10 / 2=5
5 % 2 = 1
5 / 2=2
2 % 2 = 0
2 / 2 =1
1 % 2 = 1 //高位
1 / 2=0
//打印一个整数的二进制转换 num代表的是被转换的整数
6.3双向循环链表
#include <stdio.h>
#include <stdlib.h>
typedef int datatype;
typedef struct node_t
{
datatype data;
struct node_t * prior;
struct node_t * next;
}link_node_t,*link_node_p;
typedef struct doublelinklist
{
link_node_p head;
link_node_p tail;
}double_list_t,*double_list_p;
int main(int argc, const char *argv[])
{
int i;
int all_num = 8;//猴子总数
int start_num = 3;//从3号猴子开始数
int kill_num = 3;//数到几杀死猴子
link_node_p h = NULL;
link_node_p pdel = NULL;//用来指向被杀死猴子的节点
printf("请您输入猴子的总数,开始号码,出局号码:\n");
scanf("%d%d%d",&all_num,&start_num,&kill_num);
//1.创建一个双向的循环链表
double_list_p p = (double_list_p)malloc(sizeof(double_list_t));//申请头指针和尾指针
if(NULL == p)
{
perror("malloc failed");
return -1;
}
p->head = p->tail = (link_node_p)malloc(sizeof(link_node_t));
if(NULL == p->tail)
{
perror("p->tail malloc failed");
return -1;
}
p->head->data = 1;
p->head->prior = NULL;
p->head->next = NULL;
//将创建n个新的节点,链接到链表的尾
for(i = 2; i <= all_num; i++)
{
link_node_p pnew = (link_node_p)malloc(sizeof(link_node_t));
if(NULL == pnew)
{
perror("pnew malloc failed");
return -1;
}
pnew->data = i;
pnew->prior = NULL;
pnew->next = NULL;
//(1)将新的节点链接到链表的尾
p->tail->next = pnew;
pnew->prior = p->tail;
//(2)尾指针向后移动,指向当前链表的尾
p->tail = pnew;
}
//(3)形成双向循环链表
p->tail->next = p->head;
p->head->prior = p->tail;
//调试程序
#if 0
while(1)
{
printf("%d\n",p->head->data);
p->head = p->head->next;
sleep(1);
}
#endif
//2.循环进行杀死猴子
h = p->head;
//(1)先将h移动到start_num处,也就是开始数数的猴子号码处
for(i = 1; i < start_num; i++)
h = h->next;
printf("start is:%d\n",h->data);
while(h->next != h)//当h->next == h 就剩一个节点了,循环结束
{
//(2)将h移动到即将杀死猴子号码的位置
for(i = 1; i < kill_num; i++)
h = h->next;
//(3)进行杀死猴子,经过上面的循环后,此时的h指向即将杀死的猴子
h->prior->next = h->next;
h->next->prior = h->prior;
pdel = h;//pdel指向被杀死猴子的位置
printf("kill is -------%d\n",pdel->data);
h = h->next;//需要移动,从杀死猴子后的下一个位置开始数
free(pdel);
pdel = NULL;
}
printf("猴王是%d\n",h->data);
return 0;
}
面试题:单向链表与双向(循环)链表的区别:
在存储空间方面:单链表需要的存储空间比双向链表的要少,因为双向链表不仅保存有指向下一个节点的指针,还保存有指向上一个节点的指针,需要较大的空间来存储双向链表的指针域。
在处理时间方面:双向链表的插入与删除操作比单链表的效率高,因为如果在后半段删除或者插入可以从后往前遍历到插入或删除位置然后进行操作。
在最末尾插入的时候,单链表需要找到需要插入位置的前一个节点,需要遍历整个链表,时间复杂度为O(n),而双向链表只需要head->tail,就可以得到最后一个节点的位置,然后就可以进行插入操作,时间复杂度为O(1)。在删除操作方面,单链表需要遍历到需要删除的节点的前一个节点,双向链表需要遍历到需要删除的节点,时间复杂度同为O(n)。
请完成如下双向链表的删除过程: (云尖软件开发笔试试题)
- p->next = p->prior->next;
p->prior = p->next->prior;
free(p);
B. p->prior = p->next;
p->next = p->prior;
free(p);
C. p->prior->next = p->next;
p->next >prior = p->prior:
free(p);
- p->prior->next = p->next->prior;
p->next->prior = p->prior->next;
free(p);
选择:C
关于链表操作,以下描述正确的有()
单链表:
struct SLL
{
struct SLL *pstNext; /* 下一结点 */
};
双链表:
struct DLL
{
struct DLL *pstNext; /* 下一结点 */
struct DLL *pstPrev; /* 前一结点 */
};
A. 单链表,以下代码可以删除节点B
pstA->pstNext = pstA->pstNext->pstNext;
free(pstA->pstNext);
B. 单链表,以下代码可以删除节点B
pstA->pstNext = pstB->pstNext;
free(pstB);
C. 双链表,以下代码可以删除节点B
pstB->pstPrev->pstNext = pstB->pstNext;
pstB->pstNext->pstPrev = pstB->pstPrev;
free(pstB);
D. 双链表,以下代码可以删除节点B
pstA->pstNext = pstA->pstNext->pstNext;
pstA->pstNext->pstNext->pstPrev = pstA;
free(pstB);
选:BC
7.树Tree
7.1 特性
7.1.1什么是树
树(Tree)是(n>=0)个节点的有限集合T,它满足两个条件:
(1) 有且仅有一个特定的称为根(Root)的节点。
- 其余的节点可以分为m(m≥0)个互不相交的有限集合T1、T2、……、Tm,其中每一个集合又是一棵树,并称为其根的子树(Subtree)。
树的特性:层次关系,一对多,每一个节点最多有一个前驱,但是可以有多个后继(根节点无前驱,叶节点无后继)
关于树的节点:和链表类似,树存储结构中也将存储的各个元素称为 "节点"。
7.1.2 关于树的术语
- 度数:一个节点的子树的个数 (一个节点有几个孩子为该节点度数)
- 树度数:树中节点的最大度数
- 叶节点或终端节点: 度数为零的节点
- 分支节点:度数不为零的节点 (A B C D E H)
- 内部节点:除根节点以外的分支节点 (去掉根和叶子)
- 节点层次: 根节点的层次为1,根节点子树的根为第2层,以此类推
- 树的深度或高度: 树中所有节点层次的最大值
例子:
7.2 二叉树
最多只有俩孩子的树,并且分为左孩子和右孩子。
7.2.1 什么是二叉树
二叉树(Binary Tree)是n(n≥0)个节点的有限集合,它或者是空集(n=0), 或者是由一个根节点以及两棵互不相交的、分别称为左子树和右子树的二叉树组成。
二叉树与普通有序树不同,二叉树严格区分左孩子和右孩子,即使只有一个子节点也要区分左右。
7.2.2 二叉树性质(重点)
- 二叉树第k(k>1)层上的节点最多为2的k-1次幂个。//2(k-1)
- 深度为k(k>=1)的二叉树最多有2的k次幂-1个节点。
//满二叉树的时候 的节点数 2k-1
- 在任意一棵二叉树中,树叶的数目比度数为2的节点的数目多一。
设度数为0的节点数为n0,度数为1的节点数为n1以及度数为2的节点数为n2,则:
总节点数为各类节点之和:n = n0 + n1 + n2
总节点数为所有子节点数加一:n = 1*n1 + 2*n2 + 1
(总节点数 n = 所有的孩子数 + 根节点数1)
0 = n2+1-n0
下面式子减上面得:n0 = n2 + 1
度数0的n0:度数为0 那么度数为0节点的子节点共有 0 个子节点
度数1的n1:度数为1 那么度数为1节点的子节点共有 n1 个子节点
度数2的n2:度数为2 那么度数为2节点的子节点共有 2*n2 子节点
7.2.3 满二叉树和完全二叉树
满二叉树: 深度为k(k>=1)时节点数为2^k - 1(2的k次幂-1)
完全二叉树: 只有最下面两层有度数小于2的节点,且最下面一层的叶节点集中在最左边的若干位置上。(先挂树的左边向右, 从上向下挂)
下面两个图不能称为完全二叉树
7.2.4 二叉树的存储结构
- 二叉树的顺序存储结构
顺序存储结构 :完全二叉树节点的编号方法是从上到下,从左到右,根节点为1号节点。设完全二叉树的节点数为n,某节点编号为i
- 当i>1(不是根节点)时,有父节点,其父节点编号为i/2;
- 当2*i<=n时,有左孩子,其编号为2*i ,否则没有左孩子,本身是叶节点;
- 当2*i+1<=n时,有右孩子,其编号为2*i+1 ,否则没有右孩子;
有n个节点的完全二叉树可以用有n+1 个元素的数组进行顺序存储,节点号和数组下标一一对应,下标为零的元素不用。
利用以上特性,可以从下标获得节点的逻辑关系。不完全二叉树通过添加虚节点构成完全二叉树,然后用数组存储,
这要浪费一些存储空间。
- 二叉树的遍历(重点)
前序:根左右
中序:左根右
后序:左右根
练习:
已知遍历结果如下,试画出对应的二叉树。
前序: A B C E H F I J D G K
中序: A H E C I F J B D K G
提示:用前序确定根节点,然后用中序找到根节点然后再找左右子。
7.2.5 二叉树的链式存储
用链表实现,基于完全二叉树规律来构建树,按照完全二叉树的编号方法,从上到下,从左到右。
第n个节点:
左子节点编号: 2 * n
右子节点编号: 2 * n + 1
可以根据左右节点编号来判断是否对二叉树构建完成
typedef struct tree_node_t
{
int data; //数据域
struct tree_node_t *lchild; //左子
struct tree_node_t *rchild; //右子
} bitree_node_t, * bitree_node_p;
- 创建二叉树
//创建二叉树,用递归函数创建。
bitree_node_p CreateBitree(int n, int i) //i根节点的编号,n:节点数
{
bitree_node_p p = (bitree_node_p)malloc(sizeof(bitree_node_t));
if(p == NULL)
{
perror("bitree_node_t CreateBitree r malloc err");
return NULL;
}
p->data = i;
if(2*i <= n)
p->lchild = CreateBitree(n, 2 * i);
else
p->lchild = NULL;
if(2*i+1 <= n)
p->rchild = CreateBitree(n, 2 * i + 1);
else
p->rchild = NULL;
return p;
}
- 前序
// 前序
void PreOrder(bitree_node_p p)
{
while (p == NULL)
return;
printf("%d ", p->data); // 根
if (p->lchild != NULL)
PreOrder(p->lchild); // 左
if (p->rchild != NULL)
PreOrder(p->rchild); // 右
}
- 中序
// 中序
void InOrder(bitree_node_p p)
{
while (p == NULL)
return;
if (p->lchild != NULL)
InOrder(p->lchild); // 左
printf("%d ", p->data); // 根
if (p->rchild != NULL)
InOrder(p->rchild); // 右
}
- 后序
// 后序
void PostOrder(bitree_node_p p)
{
while (p == NULL)
return;
if (p->lchild != NULL)
PostOrder(p->lchild); // 左
if (p->rchild != NULL)
PostOrder(p->rchild); // 右
printf("%d ", p->data); // 根
}
main.c
int main(int argc, char const *argv[])
{
bitree_node_p p = CreateBitree(7, 1);
printf("前序遍历");
PreOrder(p);
printf("\n");
printf("中序遍历");
InOrder(p);
printf("\n");
printf("后序遍历");
PostOrder(p);
printf("\n");
return 0;
}
7.4 层次遍历
层次遍历(队列思想)一定要懂
练习:
(2) 深度为8的二叉树,其最多有( ) 个节点,第8层最多有( )个节点
(网易)
(3) 数据结构中,沿着某条路线,一次对树中每个节点做一次且仅做一次访问,对二叉树的节点从1开始进行连续编号,要求每个节点的编号大于其左、右孩子的编号,同一节点的左右孩子中,其左孩子的编号小于其右孩子的编号,可采用( )次序的遍历实现编号(网易)
A 先序 B 中序 C 后序 D 从根开始层次遍历
(4)一颗二叉树的 前序: A B D E C F, 中序:B D A E F C 问树的深度是 ( ) (网易)
A 3 B 4 C 5 D 6