王道考研数据结构笔记

2022年4月16日
整理了这篇博客的pdf版本,但是缺的部分可能没法补上啦,把毕业的事忙完了可能!可以!对着书把笔记再整理一遍!所以所以,如果有需要pdf版本的可以私信我(可能回得比较慢,也可以直接加QQ: 812051061)。

大家加油!一定会成功的!

王道考研数据结构笔记

第二章 线性表

2.1 线性表的定义和基本操作

要点:

  1. 线性表的基本操作——创销、增删、改查
  2. 传入参数时,何时要用引用 &

2.2 线性表的顺序表示

2.2.1 顺序表的定义

  1. 顺序表的实现———静态分配
#include <stdio.h>
#define MaxSize 10      //定义最大长度 
typedef struct{
   
   
    int data[MaxSize];  //用静态的“数组”存放数据元素 ElemType:int
    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的空间
                       //包括MaxSize*sizeof(ElemType)和存储length的空间
    InitList(L);       //初始化这个顺序表
    //...
    return 0;
}
  1. 顺序表的实现——动态分配

malloc函数:

L.data = (ElemType*)malloc(sizeof(ElemType)*InitSize) 其中(ElemType*)可强制转换数据类型

#include <stdlib.h> //malloc,free函数的头文件
#define InitSize 10 //默认的最大长度

typedef struct{
   
   
    int *data;       //指示动态分配数组的指针
    int MaxSize;    //顺序表的最大容量
    int length;     //顺序表的当前长度
}SeqList;

void InitSize(SeqList &L){
   
   
    L.data = (int*)malloc(sizeof(int)*InitSize);  //用malloc函数申请一片连续的存储空间
    L.length = 0;
    L.MaxSize = InitSize;
}

int main(){
   
   
    SeqList L;
    InitSize(L);
    //...其余操作
    IncreaseSize(L,5);
    return 0;
}

//增加动态数组的长度
void IncreaseSize(SeqList &L, int len){
   
   
    int *p=L.data
    L.data = (int*)malloc((L.MaxSize+len)*sizeof(int));
    for(int i=0; i<L.length; i++){
   
   
        L.data[i] = p[i]         //将数据复制到新区域
    }
    L.MaxSize = L.MaxSize + len; //顺序表最大长度增加len
    free(p);                     //释放原来的内存空间
}

2.2.2 顺序表上基本操作的实现 (插入和删除)

  1. 顺序表基本操作——插入

ListInsert(&L,i,e)

基于静态分配的代码实现

#define MaxSize 10      //定义最大长度 
typedef struct{
   
   
    int data[MaxSize];  //用静态的“数组”存放数据元素 ElemType:int
    int Length;         //顺序表的当前长度
}SqList;                //顺序表的类型定义

//基本操作——在L的位序i处插入元素e
bool ListInsert(SqList &L, int i, int e){
   
    
    //判断i的范围是否有效
    if(i<1||i>L.length+1) 
        return false;
    if(L.length>MaxSize) //当前存储空间已满,不能插入  
        return false;

    for(int j=L.length; j>i; j--){
   
       //将第i个元素及其之后的元素后移
        L.data[j]=L.data[j-1];
    }
    L.data[i-1]=e;  //在位置i处放入e
    L.length++;      //长度加1
    return true;
}

int main(){
   
   
    SqList L;          //声明一个顺序表
    InitList(L);       //初始化这个顺序表
    //...插入几个元素
    ListInsert(L,3,3);
    return 0;
}

时间复杂度分析

  • 关注最深层循环语句——L.data[j]=L.data[j-1]的执行次数与问题规模n——L.length的关系;
  • 最好情况:插入表尾,不需要移动元素,i=n+1,循环0次;最好时间复杂度 = O(1)
  • 最坏情况:插入表头,需要将原有的n个元素全都向后移动,i=1,循环n次;最坏时间复杂度 = O(n)
  • 平均情况:假设新元素插入到任何一个位置的概率p(=1/n+1)相同
插入到第i个位置 循环次数
1 n
2 n-1
3 n-2
n+1 0

平均循环次数 = np + (n-1)p + (n-2)p + … + 1×p = [ n(n+1)/2 ]×[ 1/(n+1) ] = n/2

平均时间复杂度 = O(n)

  1. 顺序表基本操作——删除

ListDelete(&L,i,e):删除表L中的第i个位置的元素,并用e返回删除元素的值
基于静态分配的代码实现

#define MaxSize 10      //定义最大长度 
typedef struct{
   
   
    int data[MaxSize];  //用静态的“数组”存放数据元素 ElemType:int
    int Length;         //顺序表的当前长度
}SqList;                //顺序表的类型定义

bool LisDelete(SqList &L, int i, int &e){
   
    // e用引用型参数 
    //判断i的范围是否有效
    if(i<1||i>L.length) 
        return false;

    e = L.data[i-1]    //将被删除的元素赋值给e

    for(int j=L.length; j>i; j--){
   
       //将第i个后的元素前移
        L.data[j-1]=L.data[j];
    }
    L.length--;      //长度减1
    return true;
}

int main(){
   
   
    SqList L;          //声明一个顺序表
    InitList(L);       //初始化这个顺序表
    //...插入几个元素
    int e = -1;        //用变量e把删除的元素“带回来”
    if(LisDelete(L,3,e))
        printf("已删除第三个元素,删除元素值=%d\n",e);
    else
        printf("位序i不合法,删除失败\n");
    return 0;
}

时间复杂度分析

  • 关注最深层循环语句——L.data[j-1]=L.data[j]的执行次数与问题规模n——L.length的关系;
  • 最好情况:删除表尾元素,不需要移动元素,i=n,循环0次;最好时间复杂度 = O(1);
  • 最坏情况:删除表头元素,需要将后续的n-1个元素全都向前移动,i=1,循环n-1次;最坏时间复杂度 = O(n);
  • 平均情况:假设删除任何一个元素(1,2,3,…,length)的概率相同 p=1/n
删除第i个元素 循环次数
1 n-1
2 n-2
3 n-3
n 0

平均循环次数 = (n-1)p + (n-2)p + … + 1×p = [ n(n-1)/2 ]×[ 1/(n) ] = n-1/2

平均时间复杂度 = O(n)

  1. 顺序表基本操作——按位查找(顺序表)

GetElem(L,i) : 按位查找操作——获取表L中第i个位置元素的值

基于静态分配的代码实现

#define MaxSize 10            //定义最大长度 
typedef struct{
   
   
    ElemType data[MaxSize];  //用静态的“数组”存放数据元素 
    int Length;              //顺序表的当前长度
}SqList;                     //顺序表的类型定义

ElemType GetElem(SqList L, int i){
   
   
    // ...判断i的值是否合法
    return L.data[i-1];      //注意是i-1
}

基于动态分配的代码实现

#define InitSize 10  //顺序表的初始长度

typedef struct{
   
   
    ElemType *data;  //指示动态分配数组的指针
    int MaxSize;     //顺序表的最大容量
    int length;      //顺序表的当前长度
}SeqList;

ElemType GetElem(SqList L, int i){
   
   
    // ...判断i的值是否合法
    return L.data[i-1]; //就算是指针也能用数组下标哦!
}

时间复杂度分析

O(1)
由于顺序表的各个数据元素在内存中连续存放,因此可以根据起始地址数据元素大小立即找到第i个元素———“随机存取”特性;

  1. 顺序表基本操作——按值查找

LocateElem(L, e): 按值查找操作,在表L中查找具有给定关键字值的元素;

基于动态分配的代码实现

#define InitSize 10            //定义最大长度 
typedef struct{
   
   
    ElemTyp *data;  //用静态的“数组”存放数据元素 
    int Length;              //顺序表的当前长度
}SqList;   

//在顺序表L中查找第一个元素值等于e的元素,并返回其位序
int LocateElem(SqList L, ElemType e){
   
   
    for(int i=0; i<L.lengthl i++)
        if(L.data[i] == e)  
            return i+1;     //数组下标为i的元素值等于e,返回其位序i+1
    return 0;               //推出循环,说明查找失败
}

Q: 如果顺序表里存放的是结构类型的数据元素,可不可以用 == 进行比较?

A: 不能!结构类型的比较,需要依次对比各个分量来判断两个结构体是否相等;

例:

typedef struct{
   
   
    int num;
    int people;
}Customer;

void test(){
   
   
    Customer a;
    Customer b;
    //...
    if (a.num == b.num && a.people == b.people){
   
   
        printf("相等");
    }else{
   
   
        printf("不相等");
    }
}

时间复杂度分析

  • 最深处循环语句: if(L.data[i] == e) 与问题规模n=L.length(表长)的关系;
  • 最好情况:查找目标元素在表头,循环1次,最好时间复杂度=O(1)
  • 最坏情况:查找目标元素在表尾,循环n次,最好时间复杂度=O(n)
  • 平均情况:假设目标元素出现在任何一个位置的概率相同,p=1/n
目标元素所在位置i 循环次数
1 1
2 2
3 3
n n

平均循环次数 = 1×1/n + 2×1/n +…+ n×1/n = [ n(n+1)/2 ] × 1/n = (n+1)/2

平均时间复杂度 = O(n)

在这里插入图片描述

2.3 线性表的链式表示

2.3.1 单链表的定义

  1. 何为单链表?
  • 链式存储
  • 每个结点存储:数据元素自身信息 & 指向下一个结点(后继)的指针
  • 优点:不要求大片连续空间,改变容量方便
  • 缺点:不可随机存取,要耗费一定空间存放指针
  1. 代码定义单链表
struct LNode{
   
               //定义单链表节点类型  LNode:结点
    ElemType data;       //每个结点存放一个数据元素 data:数据域
    struct LNode *next;  //指针指向下一个结点 next:指针域
};

增加一个新的结点:在内存中申请一个结点所需的空间,并用指针p指向这个结点

struct LNode* p = (struct LNode*) malloc(sizeof(struct LNode))

如果每次都要写struct很麻烦,所以可以利用typedef关键字——数据类型重命名:type<数据类型><别名>

Eg:

typedef int zhengshu;
typedef int *zhengshuzhizhen;  //指向int型的指针

上面操作可以化简为:

typedef struct LNode LNode;

LNode* p = (LNode*) malloc(sizeof(LNode))

最简洁代码实现:

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

以上代码等同于:

struct LNode{
   
              
    ElemType data;       
    struct LNode *next; 
};

typedef struct LNode LNode; //重命名
typedef struct LNode *LinkList; 

要表示一个单链表时,只需声明一个头指针L,指向单链表的第一个结点:

LNode *L;    // 声明一个指向单链表第一个结点的指针,强调这是结点
//或者
LinkList L;  // 声明一个指向单链表第一个结点的指针,强调这是链表
  1. 两种实现方法
  • 不带头结点的单链表
typedef struct LNode{
   
   
    ElemType data;
    struct LNode *next;
}LNode, *LinkList;

//初始化一个空的单链表
bool InitList(LinkList &L){
   
     //注意用引用 &
    L = NULL; //空表,暂时还没有任何结点;
    return true;
}

void test(){
   
   
    LinkList L;  //声明一个指向单链表的指针: 头指针
    //初始化一个空表
    InitList(L);
    //...
}

//判断单链表是否为空
bool Empty(LinkList L){
   
   
    if (L == NULL)
        return true;
    else
        return false;
}

  • 带头结点的单链表
typedef struct LNode{
   
   
    ElemType data;
    struct LNode *next;
}LNode, *LinkList;

//初始化一个单链表(带头结点)
bool InitList(LinkList &L){
   
     
    L = (LNode*) malloc(sizeof(LNode));  //头指针指向的结点——分配一个头结点(不存储数据)
    if (L == NULL)          //内存不足,分配失败
        return false;
    L -> next = NULL;       //头结点之后暂时还没有结点
    return true;
}

void test(){
   
   
    LinkList L;  //声明一个指向单链表的指针: 头指针
    //初始化一个空表
    InitList(L);
    //...
}

//判断单链表是否为空(带头结点)
bool Empty(LinkList L){
   
   
    if (L->next == NULL)
        return true;
    else
        return false;
}

不带头结点 V.S. 带头结点

  • 不带头结点:写代码麻烦!对第一个数据节点和后续数据节点的处理需要用不同的代码逻辑,对空表和非空表的处理也需要用不同的代码逻辑; 头指针指向的结点用于存放实际数据;
  • 带头结点:头指针指向的头结点不存放实际数据,头结点指向的下一个结点才存放实际数据;

2.3.2 单链表上基本操作的实现

1. 单链表的插入

  • 按位序插入 (带头结点)

ListInsert(&L, i, e): 在表L中的第i个位置上插入指定元素e = 找到第i-1个结点(前驱结点),将新结点插入其后;其中头结点可以看作第0个结点,故i=1时也适用。

代码实现

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

//在第i个位置插入元素e(带头结点)
bool ListInsert(LinkList &L, int i, ElemType e){
   
     
    //判断i的合法性, i是位序号(从1开始)
    if(i<1)
        return False;
    
    LNode *p;       //指针p指向当前扫描到的结点 
    int j=0;        //当前p指向的是第几个结点
    p = L;          //L指向头结点,头结点是第0个结点(不存数据)

    //循环找到第i-1个结点
    while(p!=NULL && j<i-1){
   
        //如果i>lengh, p最后会等于NULL
        p = p->next;             //p指向下一个结点
        j++;
    }

    if (p==NULL)                 //i值不合法
        return false;
    
    //在第i-1个结点后插入新结点
    LNode *s = (LNode *)malloc(sizeof(LNode)); //申请一个结点
    s->data = e;
    s->next = p->next;
    p->next = s;                 //将结点s连到p后,后两步千万不能颠倒qwq

    return true;
}

时间复杂度分析

最好情况:插入第1个位置 O(1)

最坏情况:插入表尾 O(n)

平均时间复杂度 = O(n)

  • 按位序插入 (不带头结点)

ListInsert(&L, i, e): 在表L中的第i个位置上插入指定元素e = 找到第i-1个结点(前驱结点),将新结点插入其后; 因为不带头结点,所以不存在“第0个”结点,因此!i=1 时,需要特殊处理——插入(删除)第1个元素时,需要更改头指针L;

代码实现

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

bool ListInsert(LinkList &L, int i, ElemType e){
   
   
    if(i<1)
        return false;
    
    //插入到第1个位置时的操作有所不同!
    if(i==1){
   
   
        LNode *s = (LNode *)malloc(size of(LNode));
        s->data =e;
        s->next =L;
        L=s;          //头指针指向新结点
        return true;
    }

    //i>1的情况与带头结点一样!唯一区别是j的初始值为1
    LNode *p;       //指针p指向当前扫描到的结点 
    int j=1;        //当前p指向的是第几个结点
    p = L;          //L指向头结点,头结点是第0个结点(不存数据)

    //循环找到第i-1个结点
    while(p!=NULL && j<i-1){
   
        //如果i>lengh, p最后会等于NULL
        p = p->next;             //p指向下一个结点
        j++;
    }

    if (p==NULL)                 //i值不合法
        return false;
    
    //在第i-1个结点后插入新结点
    LNode *s = (LNode *)malloc(sizeof(LNode)); //申请一个结点
    s->data = e;
    s->next = p->next;
    p->next = s;          
    return true;

}

除非特别声明,否则之后的代码都默认为带头结点哦,做题注意审题

  • 指定结点的后插操作

InsertNextNode(LNode *p, ElemType e): 给定一个结点p,在其之后插入元素e; 根据单链表的链接指针只能往后查找,故给定一个结点p,那么p之后的结点我们都可知,但是p结点之前的结点无法得知;

代码实现

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

bool InsertNextNode(LNode *p, ElemType e){
   
   
    if(p==NULL){
   
   
        return false;
    }

    LNode *s = (LNode *)malloc(sizeof(LNode));
    //某些情况下分配失败,比如内存不足
    if(s==NULL)
        return false;
    s->data = e;          //用结点s保存数据元素e 
    s->next = p->next;
    p->next = s;          //将结点s连到p之后

    return true;
}                         //平均时间复杂度 = O(1)


//有了后插操作,那么在第i个位置上插入指定元素e的代码可以改成:
bool ListInsert(LinkList &L, int i, ElemType e){
   
     
    if(i<1)
        return False;
    
    LNode *p;       //指针p指向当前扫描到的结点 
    int j=0;        //当前p指向的是第几个结点
    p = L;          //L指向头结点,头结点是第0个结点(不存数据)

    //循环找到第i-1个结点
    while(p!=NULL && j<i-1){
   
        //如果i>lengh, p最后会等于NULL
        p = p->next;             //p指向下一个结点
        j++;
    }

    return InsertNextNode(p, e)
}
  • 指定结点的前插操作

Q: 如何找到p结点的前驱节点?

A: 传入头指针L!就可以知道整个链表的信息了!

InsertPriorNode(LinkList L, LNode *p, ElemType e):循环查找p的前驱q,再对q进行后插操作,时间复杂度为O(n);

Q: 那如果不传入头指针L呢?

不传入头指针L的代码实现

//前插操作:在p结点之前插入元素e
bool InsertPriorNode(LNode *p, ElenType 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连到p之后
    s->data = p->data; //将p中元素复制到s
    p->data = e;       //p中元素覆盖为e

    return true;
}  //时间复杂度为O(1)

王道书版本代码

bool InsertPriorNode(LNode *p, LNode *s){
   
   
    if(p==NULL || S==NULL)
        return false;
    
    s->next = p->next;
    p->next = s;  ///s连接到p
    ELemType temp = p->data;  //交换数据域部分
    p->data = s->data;
    s->data = temp;

    return true;
}
 

2. 单链表的删除

  • 按位序删除(带头结点)

ListDelete(&L, i, &e): 删除操作,删除表L中第i个位置的元素,并用e返回删除元素的值;头结点视为“第0个”结点;

思路:找到第i-1个结点,将其指针指向第i+1个结点,并释放第i个结点;

代码实现

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

bool ListDelete(LinkList &L, int i, ElenType &e){
   
   
    if(i<1) return false;

    LNode *p;       //指针p指向当前扫描到的结点 
    int j=0;        //当前p指向的是第几个结点
    p = L;          //L指向头结点,头结点是第0个结点(不存数据)

    //循环找到第i-1个结点
    while(p!=NULL && j<i-1</
评论 42
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值