前言:为啥学习数据结构:1、学习C语言是为了让大家如何去写程序2、学习数据结构是为了让大家简洁、高效的去写程序。
目录
1、相关概念
数据结构研究的是数据的逻辑结构、存储结构及其操作。
数据:计算机处理的对象(数据)已不再单纯是数值,更多的是一组数据。
逻辑结构:
1对1 -----线性关系
1对多-----树形关系(1对2---二叉树)
多对多----网状关系 ----图
存储结构:
顺序存储---顺序表
链式存储---链表
索引存储
hash存储---hash表
操作:
创建、插入、显示、删除、修改、查找
2、线性表
顺序表
1、顺序表的特点
(1)顺序并且连续存储、访问方便
(2)大小固定
(3)表满不能存、表空不能取
优点:访问方便
缺点:插入、删除不方便都需要移动元素
2、创建顺序表
3、向顺序表中插入数据
4、显示
5、删除
6、销毁
链表
1、单项链表
(1) 链表的特点
- 申请的空间可以不连续
- 访问不方便
- 插入、删除不需要移动元素
(2) 相关概念
链表的分类:
有没有头结点:带头结点的链表、不带头结点的链表
指针域是双向还是单向:单向链表、双向链表
尾结点是否指向头结点:循环链表、不循环链表
(3) 向链表中插入数据
一:头插法
二:尾插法
三:中间插入
(4) 显示
(5) 根据位置删除链表中的数据
一:头删法
二:尾删法
三:中间删除
(6) 销毁
2、双向链表
(1) 创建链表
(2) 插入链表
(3)删除链表
特殊的线性表
1、栈
(1) 栈的特征
一:栈是限制在一端(栈顶)进行插入操作和删除操作的线性表;二:先入后出
(2) 栈的存储
一:创建顺序栈
二:入栈
/**
进栈
**/
bool Push(SqStack &S,int x){
if(S.top==MaxSize-1) //栈顶指针指向最后一个,栈满,报错,因为数组下标从0开始,数组下标最大值为Max-1
return false;
S.data[++S.top] = x; //应熟练掌握++i和i++的区别,这里因为top指针指向的是栈目前最后一个元素,需要将指针移到下一个再装填新元素,否则会覆盖,所以使用++S.top。
return true;
}
三:出栈
/**
出栈
*/
bool Pop(SqStack &S,int &x){
if(S.top==-1)
return false;
x = S.data[S.top--]; //将栈顶元素弹出,指针往下-1。
return true;
}
2、队列
(1) 队列的特征:一:队列允许在两端进行操作,在队尾插入,在队头删除;二:先进先出
(2) 队列的存储(循环队列)
一:创建队列
//初始化
void InitQueue (SqQueue &Q)
{
//构造一个空队列
Q.base =new QElemType[MAXQSIZE];
Q.front=Q.rear=0;
}
二:入队
//循环队列入队
Status EnQueue(SqQueue &Q,QElemType e)
{
if((Q.rear+1)%MAXQSIZE==Q.front) return ERROR;
Q.base[Q.rear]=e;
Q.rear = (Q.rear+1) % MAXQSIZE;
return OK;
}
三:出队
//循环队列出队
Status DeQueue (SqQueue &Q,QElemType &e)
{
if(Q.front==Q.rear) return ERROR;
e=Q.base[Q.front];
Q.front=(Q.front+1) % MAXQSIZE;
return OK;
}
四:销毁队列
//销毁队列
void DestroyQueue(SqQueue &Q)
{
if(Q.base)
free(Q.base);
Q.base = NULL;
Q.front = Q.rear = 0;
}
3、树
树的相关概念
1、度数
一个节点的子树的个数称为该节点的度数,一棵树的度数是指该树中节点的最大度数。
2、深度
节点的层数等于父节点的层数加一,根节点的层数定义为一。树中节点层数的最大值称为该树的高度或深度。
3、边数
一个节点系列k1,k2, ……,ki,ki+1, ……,kj,并满足ki是ki+1的父节点,就称为一条从k1到kj的路径,路径的长度为j-1,即路径中的边数。
二叉树
1、什么是二叉树
二叉树(Binary Tree)是n(n≥0)个节点的有限集合,它或者是空集(n=0),或者是由一个根节点以及两棵互不相交的、分别称为左子树和右子树的二叉树组成。
2、二叉树的性质
(1).二叉树第i(i≥1)层上的节点最多为2i-1个。
(2).深度为k(k≥1)的二叉树最多有2k-1个节点。
(3).在任意一棵二叉树中,树叶的数目比度数为2的节点的数目多一。
总节点数为各类节点之和:n = n0 + n1 + n2
总节点数为所有子节点数加一:n = n1 + 2*n2 + 1
故得:n0 = n2 + 1 ;
3、满二叉树
深度为k(k≥1)时有2^k-1个节点的二叉树就是满二叉树。
4、完全二叉树
在满二叉树的基础上从右到左,从下到上,依次删除若干个节点的二叉树就是完全二叉树。
二叉树的存储
1、顺序存储
2、链式存储
(1) 定义二叉树的类型
typedef char BTDataType;
typedef struct BinaryTreeNode
{
struct BinaryTreeNode* left; // 指向当前节点左孩子
struct BinaryTreeNode* right; // 指向当前节点右孩子
BTDataType data; // 当前节点值域
}BTNode;
(2) 创建二叉树
BTNode* CreateTreeNode(BTDataType x)
{
BTNode* node = (BTNode*)malloc(sizeof(BTNode));
node->data = x;
node->left = NULL;
node->right = NULL;
return node;
}
(3) 给二叉树插入节点
举例:创建一颗有序树,54 13 78 89 50 34 45 85 90 67
序列中第一数作为根结点,比根结点大的作为它的右子树,比根结点小的左子树
二叉树的遍历
1、递归遍历
(1) 先序遍历
根据根,左子树,右子树的顺序访问二叉树的所以值
void PrevOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
//根 左子树 右子树
printf("%c ", root->data);
PrevOrder(root->left);
PrevOrder(root->right);
}
1.先判断节点是否为空,空就返回。
2.前序遍历,先访问根节点,那就是节点当前的值,在访问左子树,再访问右子树,使用递归的方法实现。
(2) 中序遍历
void InOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
InOrder(root->left);
printf("%c ", root->data);
InOrder(root->right);
}
1.判断是否为空树。
2.先递归访问左树,再访问节点自身,在访问右树。
(3) 后续遍历
void PostOrder(BTNode* root)
{
if (root == NULL)
{
printf("NULL ");
return;
}
PostOrder(root->left);
PostOrder(root->right);
printf("%c ", root->data);
}
1.判断节点是否为空,空就返回。
先访问左子树,再访问右子树,再访问自己。
2、非递归遍历
哈夫曼树
结点的带权路径长度指的是从树根到该结点的路径长度和结点上权的乘积。树的带权路径长度是指所有叶子节点的带权路径长度之和,记作 WPL 。WPL最小的二叉树就是最优二叉树,又称为赫夫曼树。
哈夫曼树编码
哈夫曼编码基于哈夫曼树而产生的一种好编码,那么编码就是左子树上的为0,右子树上的为1,再自根结点扫描下来到叶子结点,输出的值就为哈夫曼编码。
4、算法
1、程序等于算法+数据结构
(1) 什么是算法
算法(Algorithm)是一个有穷规则(或语句、指令)的有序集合
算法:解决问题的方法步骤
(2) 什么是程序
用计算机语言对算法的具体实现
(3) 算法和数据结构
算法的设计: 取决于选定的逻辑结构 (1对1(线性表),1对多(树),多对多)
算法的实现: 依赖于采用的存储结构 (顺序存储,链式存储,索引存储,散列存储)
2、算法的特性
(1) 有穷性 —— 算法执行的步骤(或规则)是有限的;
(2) 确定性 —— 每个计算步骤无二义性;
(3) 可行性 —— 每个计算步骤能够在有限的时间内完成;
(4) 输入 —— 算法有一个或多个外部输入;
(5) 输出 —— 算法有一个或多个输出。
3、如何评判一个算法的好坏
(1) 时间复杂度
问题的规模 :输入数据量的大小,用n来表示。
算法的时间复杂度 :算法消耗时间,它是问题规模的函数 T(n)
一:计算O的方法
(1).根据位置规模n写出表达式 f(n)=n^2/2+n/2
(2).如果有常数项,将其置为1 (当f(n)表达式中只有常数项的时候,有意义)
(3).只保留最高项,其他项舍去 f(n)=n^2/2
(4).如果最高项系数不为1,将其置为1 f(n)=n^2
T(n)=O(n^2) ---->平方级
二:T(n) 的量级
(2) 空间复杂度
一个程序的空间复杂度是指运行完一个程序所需内存的大小。
利用程序的空间复杂度,可以对程序的运行所需的内存有个预先估计。
程序执行时所需存储空间包括以下两部分:
一:固定部分。这部分空间的大小与输入/输出的数据的个数多少、数值无关。主要包括指令空间(即代码空间)、数据空间(常量、简单变量)等所占的空间。这部分属于静态空间。
二:可变空间。这部分空间主要包括动态分配的空间,以及递归栈所需的空间等。这部分的空间大小与算法有关。
我们在写代码时,完全可以用空间来换取时间,比如字典树、哈希等都是这个原理。HashMap.get()、put()都是O(1)的时间复杂度。
空间复杂度为O(1):有的算法只需要占用少量的临时工作单元,而且不随问题规模的大小而改变,我们称这种算法是“就地”执行的,是节省存储的算法,空间复杂度为O(1)。
空间复杂度为O(n):有的算法需要占用的临时工作单元与解决问题的规模n有关,它随着n的增大而增大,当n较大时,将占用较多的存储单元,例如快速排序和归并排序算法就属于这种情况。
常见的空间复杂度就是O(1)、O(n)、O(n^2),像是O(logn)、O(nlogn)对数阶的复杂度平时都用不到,而且空间复杂度分析比时间复杂度分析要简单很多。
(3) 容易理解、容易编程和调试、容易维护
4、查找算法
查找算法主要学习顺序查找,二分查找,哈希查找。
(1) 顺序查找
说明:顺序查找适合于存储结构为顺序存储或链接存储的线性表。
基本思想:顺序查找也称为线形查找,属于无序查找算法。从数据结构线形表的一端开始,顺序扫描,依次将扫描到的结点关键字与给定值k相比较,若相等则表示查找成功;若扫描结束仍没有找到关键字等于k的结点,表示查找失败。
复杂度分析:
查找成功时的平均查找长度为:(假设每个数据元素的概率相等) ASL = 1/n(1+2+3+…+n) = (n+1)/2 ;
当查找不成功时,需要n+1次比较,时间复杂度为O(n);
所以,顺序查找的时间复杂度为O(n)。
(2) 二分查找
说明:元素必须是有序的,如果是无序的则要先进行排序操作。
基本思想:也称为是折半查找,属于有序查找算法。用给定值k先与中间结点的关键字比较,中间结点把线形表分成两个子表,若相等则查找成功;若不相等,再根据k与该中间结点关键字的比较结果确定下一步查找哪个子表,这样递归进行,直到查找到或查找结束发现表中没有这样的结点。
复杂度分析:最坏情况下,关键词比较次数为log2(n+1),且期望时间复杂度为O(log2n);
注:折半查找的前提条件是需要有序表顺序存储,对于静态查找表,一次排序后不再变化,折半查找能得到不错的效率。但对于需要频繁执行插入或删除操作的数据集来说,维护有序的排序会带来不小的工作量,那就不建议使用。——《大话数据结构》
(3) 哈希查找
什么是哈希表(Hash)?
我们使用一个下标范围比较大的数组来存储元素。可以设计一个函数(哈希函数, 也叫做散列函数),使得每个元素的关键字都与一个函数值(即数组下标)相对应,于是用这个数组单元来存储这个元素;也可以简单的理解为,按照关键字为每一个元素"分类",然后将这个元素存储在相应"类"所对应的地方。但是,不能够保证每个元素的关键字与函数值是一一对应的,因此极有可能出现对于不同的元素,却计算出了相同的函数值,这样就产生了"冲突",换句话说,就是把不同的元素分在了相同的"类"之中。后面我们将看到一种解决"冲突"的简便做法。
总的来说,"直接定址"与"解决冲突"是哈希表的两大特点。
什么是哈希函数?
哈希函数的规则是:通过某种转换关系,使关键字适度的分散到指定大小的的顺序结构中,越分散,则以后查找的时间复杂度越小,空间复杂度越高。
算法思想:哈希的思路很简单,如果所有的键都是整数,那么就可以使用一个简单的无序数组来实现:将键作为索引,值即为其对应的值,这样就可以快速访问任意键的值。这是对于简单的键的情况,我们将其扩展到可以处理更加复杂的类型的键。
算法流程:
1)用给定的哈希函数构造哈希表;
2)根据选择的冲突处理方法解决地址冲突;
常见的解决冲突的方法:拉链法和线性探测法。详细的介绍可以参见:浅谈算法和数据结构: 十一 哈希表。
3)在哈希表的基础上执行哈希查找。
哈希表是一个在时间和空间上做出权衡的经典例子。如果没有内存限制,那么可以直接将键作为数组的索引。那么所有的查找时间复杂度为O(1);如果没有时间限制,那么我们可以使用无序数组并进行顺序查找,这样只需要很少的内存。哈希表使用了适度的时间和空间来在这两个极端之间找到了平衡。只需要调整哈希函数算法即可在时间和空间上做出取舍。
复杂度分析:
单纯论查找复杂度:对于无冲突的Hash表而言,查找复杂度为O(1)(注意,在查找之前我们需要构建相应的Hash表)。
5、排序算法
排序算法主要学习选择排序,插入排序,快排。
(1) 选择排序
选择排序是一种简单直观的排序算法,无论什么数据进去都是 O(n²) 的时间复杂度。所以用到它的时候,数据规模越小越好。唯一的好处可能就是不占用额外的内存空间了吧。具体来说,假设长度为n的数组arr,要按照从小到大排序,那么先从n个数字中找到最小值min1,如果最小值min1的位置不在数组的最左端(也就是min1不等于arr[0]),则将最小值min1和arr[0]交换,接着在剩下的n-1个数字中找到最小值min2,如果最小值min2不等于arr[1],则交换这两个数字,依次类推,直到数组arr有序排列。算法的时间复杂度为O(n^2)。
选择排序算法的原理如下:
1.首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置。
2.再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。
3.重复第二步,直到所有元素均排序完毕。
核心代码如下:
// 自定义方法:交换两个变量的值
void swap(int *a,int *b)
{
int temp = *a;
*a = *b;
*b = temp;
}
/* 选择排序 */
void selection_sort(int arr[], int len)
{
int i,j;
for (i = 0 ; i < len - 1 ; i++) {
int min = i;
for (j = i + 1; j < len; j++) { // 遍历未排序的元素
if (arr[j] < arr[min]) { // 找到目前最小值
min = j; // 记录最小值
}
}
swap(&arr[min], &arr[i]); //做交换
/*if (index != i) // 不用自定义函数时可以用选择下面方式进行交换
{
temp = arr[i];
arr[i] = arr[index];
arr[index] = temp;
}*/
}
}
(2) 插入排序
插入排序(Insertion-Sort)的算法描述是一种简单直观的排序算法。它的工作原理是通过构建有序序列,对于未排序数据,在已排序序列中从后向前扫描,找到相应位置并插入。例如要将数组arr=[4,2,8,0,5,1]排序,可以将4看做是一个有序序列,将[2,8,0,5,1]看做一个无序序列。无序序列中2比4小,于是将2插入到4的左边,此时有序序列变成了[2,4],无序序列变成了[8,0,5,1]。无序序列中8比4大,于是将8插入到4的右边,有序序列变成了[2,4,8],无序序列变成了[0,5,1]。以此类推,最终数组按照从小到大排序。该算法的时间复杂度为O(n^2)。
插入排序算法的原理如下:
1.从第一个元素开始,该元素可以认为已经被排序;
2.取出下一个元素,在已经排序的元素序列中从后向前扫描;
3.如果该元素(已排序)大于新元素,将该元素移到下一位置;
4.重复步骤3,直到找到已排序的元素小于或者等于新元素的位置;
5.将新元素插入到该位置后;
6.重复步骤2~5。
核心代码如下:
/* 插入排序 */
void insertion_sort(int arr[], int len){
int i,j,key;
for (i=1;i<len;i++){
key = arr[i];
j=i-1;
while((j>=0) && (arr[j]>key)) {
arr[j+1] = arr[j];
j--;
}
arr[j+1] = key;
}
}
(3) 快排
快速排序通常明显比其他 Ο(nlogn) 算法更快,因为它的内部循环(inner loop)可以在大部分的架构上很有效率地被实现出来。快速排序的基本思想是:通过一趟排序将待排记录分割成独立的两部分,其中一部分记录的关键字均比另一部分记录的关键字小,则可分别对这两部分记录继续进行排序,已达到整个序列有序。一趟快速排序的具体过程可描述为:从待排序列中任意选取一个记录(通常选取第一个记录)作为基准值,然后将记录中关键字比它小的记录都安置在它的位置之前,将记录中关键字比它大的记录都安置在它的位置之后。这样,以该基准值为分界线,将待排序列分成的两个子序列。它是处理大数据最快的排序算法之一了。该算法时间复杂度为O(n log n)。
快速排序算法的原理如下:
1.从数列中挑出一个元素,称为 “基准”(pivot);
2.重新排序数列,所有元素比基准值小的摆放在基准前面,所有元素比基准值大的摆在基准的后面(相同的数可以到任一边)。在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
3.递归地(recursive)把小于基准值元素的子数列和大于基准值元素的子数列排序。
核心代码如下:
// 快速排序
void QuickSort(int arr[], int start, int end)
{
if (start >= end)
return;
int i = start;
int j = end;
// 基准数
int baseval = arr[start];
while (i < j)
{
// 从右向左找比基准数小的数
while (i < j && arr[j] >= baseval)
{
j--;
}
if (i < j)
{
arr[i] = arr[j];
i++;
}
// 从左向右找比基准数大的数
while (i < j && arr[i] < baseval)
{
i++;
}
if (i < j)
{
arr[j] = arr[i];
j--;
}
}
// 把基准数放到i的位置
arr[i] = baseval;
// 递归
QuickSort(arr, start, i - 1);
QuickSort(arr, i + 1, end);
}