王道考研线性表部分笔记

单链表

用代码定义一个单链表:

  1. 定义一个结点类型
typedef struct Node        //结点
{
	ElemType data;     //数据域
	struct Node* next; //指针域
}Node;

2.定义一个单链表L

//表示一个单链表时,只声明一个头指针L,指向链表的第一个结点,通过这个指针就可以确定整条链表
//因此指向第一个结点的指针L,也可以用来指代这个单链表
typedef struct Node* Linklist;  //这是一个指向结点的指针

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

struct Node * p = (struct Node *)malloc(sizeof(struct Node));

// Node * L; //声明指向链表的第一个结点的指针L,这种方式强调这是一个结点
// LinkList L;// 声明指向链表的第一个结点的指针L ,两者是等价的,这个代码可读性更强一些,强调这是一个单链表

1. 初始化一个单链表

不带头结点的单链表

typedef struct Node{  //定义单链表结点类型
    ElemType data;    //每个结点存放一个数据元素
    struct Node *next;//指向下一个结点
}Node, *LinkList;

//初始化一个空的链表
bool InitList(Linklist & L){  //传入引用,修改链表本身而不是拷贝
    //防止脏数据
    L = NUll;    //空表,暂时还没有任何结点
    return true;
}

void test(){ //测试程序

//*******注意:此处并没有创建一个结点*********//
    LinkList L;  //声明一个指向单链表的指针
    //初始化一个空表
    InitList(L);
    //....后续代码...
}


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

带头结点的情况

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

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

void test(){

    LinkList L;  //声明一个指向单链表的指针
    //初始化一个空表
    InitList(L);
    //....后续代码...
}


//判断单链表是否为空
bool Empty(LinkList L){
    return (L->next == NULL); //
}

使用带头结点的单链表会更方便
在这里插入图片描述

总结

在这里插入图片描述

2. 单链表的插入和删除

插入

按位序插入(带头结点)

在第i个位置前插入元素e(带头结点)

bool ListInsert(LinkList &L, int i,Elemtype e){
    if(i < 1)         //位序i< 1不合法
        return false;
    Node *p;     //指针p用来指向当前扫描到的结点
    int j = 0;  //用来记录指针p指向的是第几个结点
    p = L;      //L指向头结点,头结点是第0个结点(不存数据)

    while (p != NULL && j < i - 1){ //循环找到第i-1个结点(在第i个位置前插入)
        p = p->next;  //p不断指向下一个结点
        j++;
    }
    if (p == NULL)  //位序i 太大了 会因为p != NULL条件不满足而跳出循环
        return false;
    Node *s = (Node *)malloc(sizeof(Nodee));
    s->data = e;
    //*******注意这两句的顺序不能颠倒********//
    s->next = p->next;
    p->next = s;    //将s结点插入到p之后
    return true;   //插入成功
}
按位序插入(不带头结点)

在第i个位置前插入元素e(不带头结点)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Yb5FWun3-1656222857236)(2022-06-24-16-52-25.png)]

bool ListInsert(LinkList &L, int i, ElemType e){
    if(i < 1)
        return false;
        //****不带头结点需要特殊处理*******//
    if (i == 1){
        Node * s = (Node *)malloc(sizeof(Node));
        s->data = e;
        s->next = L;
        L = s;
        return ture;
    }
    Node * p;  //指向当前结点
    int j = 1; //记录p指向第几个结点
    p = L;    //p指向第一个结点(注意不是头结点)

    while(p != NULL && j < i - 1){
        p = p->next;
        j++;
    }
    if(p == NULL)
        return false;  //i过大超出范围,是的p = NULL跳出循环
    Node *s = (Node * )malloc(sizeof(Node));
    s->data = e;
    s->next = p->next;
    p->next = s;
    return ture;
}
指定结点的后插操作

后插操作:在p结点之后插入元素e

bool InsertNextNode(Node *p, ElemType e){
    if (p == NULL)      
        return false;
    Node *s = (Node *)malloc((sizeof(Node)));
    if (S == NULL) //内存分配失败
        return false;
    s->data = e;//用结点s保存数据e
    s->next = p->next;//将结点s连到结点p之后
    p->next = s;
    return turel;
}

在第i个位置插入e可以通过调用 在p结点之后插入元素e来实现

bool ListInsert(LinkList &L, int i,Elemtype e){

    //*******找到第i-1个结点p********//
    if(i < 1)         //位序i< 1不合法
        return false;
    Node *p;     //指针p用来指向当前扫描到的结点
    int j = 0;  //用来记录指针p指向的是第几个结点
    p = L;      //L指向头结点,头结点是第0个结点(不存数据)

    while (p != NULL && j < i - 1){ //循环找到第i-1个结点(在第i个位置前插入)
        p = p->next;  //p不断指向下一个结点
        j++;
    }
    //在p结点后面插入元素e
    return InsertNextNode(p, e);
}
指定结点的前插操作

指定p结点时,由于只有后继信息,因此不能找到p前面一个结点进行插入
于是有两种方案

方案一:引入头指针L,通过遍历链表的方式,找到p结点的前驱结点,时间复杂度为O(n)
在这里插入图片描述

方案二:极限换家骚操作,在p结点后面插入结点s,将p的内容转移到s,再将结点p换成待插入的内容e,时间复杂度为O(1)

删除

按位序删除(带头结点)

ListDelete(&L,i,&e):删除操作。删除表L中第i个位置的元素,并用e返回删除元素的值。
在这里插入图片描述

最坏、平均时间复杂度:O(n)
最好时间复杂度: O(1)

bool ListDelete(LinkList &L, int i, ElemType &e){
    if(i < 1)     //i值过小
        return false;
    Node *p;  //指针p指向当前扫描到的结点
    int j = 1;//当前p指向第几个结点
    p = L;    //L指向头结点,头结点是第0个结点(不存数据)
    while(p != NULL && j < i - 1){ //循环找到第 i-1 个结点
        p = p->next;
        j++;
    }                        //退出循环时p找到第i-1个结点
    if (p == NULL) //i值过大
        return false;
    Node *q = p->next;      //令q指向被删除结点
    e = q->data;            //用e返回元素的值
    p-next = q->next;       //令*q结点从链中断开
    free(q)                 //释放结点的存储空间
    return ture;            //删除成功
}
指定结点的删除

这里也是一种换家策略,时间复杂度O(1)

bool DeleteNode(Node * p){
    if (p == NULL)
        return false;
    Node * q = p->next; //令q指向*p的后继节点
    p->data = q->data; //将后继节点交换收据保存到p结点中
    p->next = q->next; //将*q结点从链中断开
    free(q);           //释放后继节点*q的存储空间
    return ture;
}

但是如果p结点是最后一个结点, p->data = q->data;会产生错误,需要特殊处理,只能从表头依次寻找p的前驱,时间复杂度O(n)
单链表的局限性:无法逆向检索,有时候不太方便

3. 单链表的查找 (本节只探讨带头结点的情况)

按位查找

GetElem(L,i):按位查找操作,获取表中第i个位置的元素的值
按位查找,返回第i个元素(带头结点),平均时间复杂度O(n)

Node * GetElem(LinkList L,int i){
    if (i < 0)
        return NULL;
    Node * p; //p指向当前结点
    int j;   //记录P指向第几个节点
    p = L;   //L指向头结点,头结点是第0个结点(不存在数据)
    while (p != NULL && p < i){
        p = p->next;
        j++;
    }
    return p;  // 当i值不合法时,返回值p是NULL
}

按值查找

LocateElem(L,e):按值查找操作,在表中查找具有给定关键字值的元素
按值查找,找到数据域==e 的结点,时间复杂度0(n)

Node * LocateElem(LinkList L,ElemType e){
    Node * p = L->next; //从第一个结点开始查找数据域为e的结点
    while(p != NULL && p->data != e){
        p = p->next;
    }
    return p; //找到后返回该结点的指针,否则返回NULL
}

求表的长度

时间复杂度O(n)

int Length(LinkList L){
    int len = 0; //统计表长
    Node * p = L;
    while (p->next != NULL){
        p = p->next;
        len++;
    }
    return len;
}

总结

在这里插入图片描述

4. 单链表的整表建立和整表删除(本节讨论带头结点的情况)

创建:

把很多个数据元素(ElemType)如何存到一个单链表中?

  • step1: 初始化一个单链表
  • step2: 每次取一个数据元素,插入到表尾/表头
尾插法建立单链表

基本思路:
1. 初始化指向当前结点的指针p和尾结点指针r
2. 初始化计数器变量i
3. 初始化带头结点的单链表L
4. 循环{
生成一个新结点赋值给p
随机生成一数字赋值给p的数据域p->data;
将p结点插入到尾结点后面并更新尾指针
}
在第i个位置插入元素e(带头结点)

//随机数输入板
bool CreateListTail(LinkList & L,int n){
    Node * p;  //p指向当前待插入结点
    Node * r;  //r指向尾结点
    int i;
    srand(time(0));
    L = (Node *)malloc(sizeof(Node));
    if (L == NULL)
        return false;
    L->next = NULL;
    r = L;
    for(i = 0; i < n; i++){
        p = (Node *)malloc(sizeof(Node));
        p->data = rand()%100 + 1; //随机生成100以内的数字
        r->next = p; //将新结点p接到当前表尾
        r = p;       //更新尾指针
    }
    r->next = NULL;
    return ture;
}

//手动输入板
bool CreateListTail(LinkList & L){
    Node * p;  //p指向当前待插入结点
    Node * r;  //r指向尾结点
    int e;
    cout <<"请输入单链表数据,输入9999退出"<<endl;
    cin >> e;
    L = (Node *)malloc(sizeof(Node));
    if (L == NULL)
        return false;
    L->next = NULL;
    r = L;
    while(e != 9999){
        p = (Node *)malloc(sizeof(Node));
        p->data = e; 
        r->next = p; //将新结点p接到当前表尾
        r = p;       //更新尾指针
        cin >> e;
    }
    r->next = NULL;
    return true;
}





头插法

L就是始终指向表头的指针

//随机数输入板
bool CreateListHead(LinkList & L,int n){
    Node * p;
    int i;
    srand(time(0));
    L = (Node *)malloc(sizeof(Node));
     if (L == NULL)
        return false;
    L->next = NULL;
    for(i = 0; i < n; i++){
        p = (Node *)malloc(sizeof(Node));
        p->data = rand() %100 + 1;
        p->next = L->next;  //插到表头
        L->next = p;
    }  
    return true;
}


//手动输入板
bool CreateListHead(LinkList & L){
    Node * p;
    int e;
    cout <<"请输入单链表数据,输入9999退出"<<endl;
    cin >> e;
    L = (Node *)malloc(sizeof(Node));
     if (L == NULL)
        return false;
    L->next = NULL;
    while(e != 9999){
        p = (Node *)malloc(sizeof(Node));
        p->data = e;
        p->next = L->next;  //插到表头
        L->next = p;
        cin >> e;
    }  
    return true;
}
头插法的重要应用:链表的逆置
//链表逆置:将传入的链表L逆置并返回一个新的链表
LinkList InversionList(const LinkList& L) {
    LinkList temp;
    Node* f = L->next; //指向L第一个结点
    Node* p;
    temp = (Node*)malloc(sizeof(Node));
    temp->next = NULL;
    while (f != NULL) {
        p = (Node*)malloc(sizeof(Node)); 
        p->data = f->data;     //结点p接受链表L的数据
        p->next = temp->next; //使用头插法将结点p插入链表temp
        temp->next = p;
        f = f->next; //链表L从前往后头
    }
    return temp;
}

总结:头插法、尾插法的核心就是初始化操作、指定节点的后插操作
注意:

  • 尾插法注意设置一个指向尾结点的指针
  • 头插法就是在头结点进行后插

删除

算法思路如下:

1.声明两个结点指针p和q
2.将第一个结点赋值给p
3.循环
(1)将p的下一个结点赋给q
(2) 释放p结点
(3) q赋值给p,p后移

bool ClearList(LinkList& L) {
    LinkList p, q;
    p = L->next;
    while (p != NULL) {
        q = p->next;  //q用来记录p下一结点的位置
        free(p);     //释放p结点
        p = q;       //p指向下一个结点
    }
    L->next = NULL;
    return true;
}

双链表

具有前驱指针和后继指针,拥有双向检索能力,使用起来更方便。
在这里插入图片描述

1. 双链表的初始化(带头结点)

//初始化双链表
typedef int ElemType;
typedef struct DNode {
    ElemType data;
    struct Dnode* prior, * next;
}DNode, * DLinkList;

//双链表初始化函数
bool InitDLinkList(DLinkList& L) {
    L = (DNode*)malloc(sizeof(DNode));  //分配一个头结点
    if (L == NULL)    //内存不足,分配失败
        return false;
    L->prior = NULL;  //头结点的prior永远指向NULL
    L->next = NULL;   //头结点之后暂时还没有分配结点
    return true;
}


void test() {
    DLinkList L;
    InitDLinkList(L);
    //后续代码
}


//******判断双链表是否为空(带头结点)*******//
//只需要判断头结点的next指针是否为空即可
bool Empty(DLinkList L){
    if (L->next == NULL)
        return true;
    else
        return false;
}
    

2. 双链表的插入

在p结点之后插入s结点,时间复杂度O(1)

//错误的示范
bool InsertNextDNode_Bad(DNode* p, DNode* s) {
    s->next = p->next;
    p->next->prior = s;  //如果p结点恰好是最后一个结点,那么p->next == NULL,这句会有一个空指针的错误
    p->next = s;
    s->prior = p;
    return true;
}

//正确示范:在p结点之后插入s结点
bool InsertNextDNode(DNode* p, DNode *s) {
    if (p == NULL || s == NULL) //非法参数
        return false;
    s->next = p->next;
    if (p->next != NULL)      //如果p结点有后继节点,
        p->next->prior = s;   //执行原本p后继节点的前插,否则跳过这句,s结点就作为最后一个节点
    p->next->prior = s;  
    p->next = s;
    s->prior = p;
    return true;
}

在这里插入图片描述

按位序插入(调用结点后插入)

//按位序插入
bool InsertDNode(DLinkList& L, int i,ElemType e) {
    if (i < 1)
        return false;
    DNode* p;
    DNode* s;   
    int j = 0;
    p = L;
    //****找到第i-1个结点*****//
    while (p != NULL && j < i - 1) {  //退出循环后p指向第i-1个结点
        p = p->next;
        j++;
    }
    if (p == NULL)
        return false;
    //*****创建待插入结点s*********//
    s = (DNode*)malloc(sizeof(DNode)); 
    s->data = e;
    //****在第i-1个结点后进行后插操作****//
    InsertNextDNode(p, s); 
    return true;
}

双链表的整表创建(调用结点后插入)

//手动输入版
//创建双链表
bool CreateListTail(DLinkList& L) {
    DNode* p;  //p指向当前待插入结点
    DNode* r;  //r指向尾结点
    int e;
    cout << "请输入双链表数据,输入999退出" << endl;
    cin >> e;
    //初始化双链表L
    if (InitDLinkList(L) == false)
        cout << "初始化双链表头结点失败";
    r = L;
    while (e != 999) {
        //*****创建新的待插入结点*******//
        p = (DNode*)malloc(sizeof(DNode));
        p->data = e;
        //****将新结点p插入到当前尾结点后面*****//
        InsertNextDNode(r, p); 
        //****更新尾指针***********//
        r = p;       
        cin >> e;
    }
    r->next = NULL;
    return true;
}

3. 双链表的删除

删除p结点的后继节点

注意这些边界条件的判断

  • 判断p结点是不是为空
  • 判断p结点后继是不是最后一个结点
  • 判断q结点后继是不是最后一个结点
//删除p结点的后继节点
bool DeleteNextDNode(DNode* p) {
    if (p == NULL) return false;
    DNode* q = p->next;           //找到p结点的后继节点q
    if (q == NULL) return false; //p没有后继,本身就是一最后一个结点
    p->next = q->next;
    if (q->next != NULL)  //q结点不是最后一个结点,是最后一个结点,就不用链接这个前驱指针了,直接跳过即可,后面是空的。
        q->next->prior = p;
    free(q);
    return true;
}

整个链表的删除

void DestroyList(DLinkList & L){
    //循环释放各个结点
    while (L->next != NULL)
        DeleteNextDNode(L);
    free(L);  //释放头结点
    L = NULL;//头指针指向NULL
}

4. 双链表的遍历

后向遍历

当p对所有结点做了处理之后(包括最后一个结点),此时,p指向尾结点的后一个结点,此时p == NULL,对这个结点不做处理,退出循环。


while (p != NULL){
    //对结点p做相应处理,如打印
    p = p->next;
}

前向遍历

当p == NULL时说明当前结点正是头结点的前一个结点,前面一个花括号的内容对头结点做了处理,之后遇到头结点前面一个结点,不做处理,直接退出循环

while (p != NULL){
     //对结点p做相应处理
     p = p->prior;
}

前向遍历(跳过头结点,不对头结点做处理)

当p->prior == NULL时说明当前结点正是头结点,这是不进入花括号处理,直接退出循环

while (p->prior != NULL){  
     //对结点p做相应处理
     p = p->prior;
}

查找

双链表不可随机存取,按位还是按值查找的操作都只能用遍历的方式实现。时间复杂度O(n).

总结

在这里插入图片描述

循环链表

循环单链表

单链表:从一个结点出发只能找到其后续结点
循环单链表的特点:从一个结点出发可以找到其它任何一个结点
在这里插入图片描述

//初始化一个循环单链表
bool InitList(LinkList& L) {
    L = (Node*)malloc(sizeof(Node));
    if (L == NULL)
        return false;
    //*******头结点的next指针指向头结点******//
    L->next = NULL;  
    return true;
}

//判断循环链表是否为空表: L->next == L
bool Empty(LinkList L) {
    if (L->next == L)
        return true;
    else
        return false;
}

//判断结点p是否为循环单链表的表尾结点
bool isTail(LinkList L,Node *p) {
    if (p->next == L)
        return true;
    else
        return false;
}


循环双链表

在这里插入图片描述

1. 初始化

typedef int ElemType;
typedef struct DNode {
	ElemType data;
	struct DNode* prior, * next;
}DNode,*DLinkList;

//初始化为空的循环双链表
bool InitDLinkList(DLinkList& L) {
	L = (DNode*)malloc(sizeof(DNode));
	if (L == NULL)
		return false;
	L->prior = L;   //头结点的 prior 指向头结点
	L->next = L;    //头结点的 next 指向头结点
	return true;
}

void test() {
	DLinkList L;
	InitDLinkList(L); //创建一个空的循环双链表
    
}

// 判断双链表是否为空
bool Empty(DLinkList L){
    if (L->next == L)
        retutn true;
    else
        return false;
}

2. 插入

在p结点之后插入s结点
不必担心p是最后一个结点,不需要做特殊处理*

bool InsertList(DNode *p,DNode *s){
    s->next = p->next;
    //**********在循环双链表中不必担心p是最后一个结点,不需要做特殊处理***************//
    p->next->prior = s;
    s->prior = p;
    p->next = s;
}

3. 删除

删除p的后继节点q
同样的,不必再担心最后一个结点的问题,实现起来要简单许多

bool DeleteNextDNode(DNode* p) {
    if (p == NULL) return false;
    DNode* q = p->next;   

    //*******不必再担心最后一个结点的问题,实现起来要简单许多********//
    p->next = q->next;
    q->next->prior = p;
    free(q);
    
    return true;
}

总结

在这里插入图片描述

静态链表

单链表:各个结点在内存中星罗棋布,散落天涯
静态链表:分配一整片连续内存,各个结点集中安置
不用指针,使用数组下标来确定下一个结点的位置
优点:增删操作不需要移动大量元素
缺点:不能随机存取,只能从头结点开始一次往后查找:容量固定不变
每个数组元素为4Byte,每个游标4B(每个结点共8B),设起始地址为e0 = addr
e1 = 存放地址为e1 = addr + 8*游标
在这里插入图片描述

用代码定义一个静态链表


//实现方式一:定义结点数组的方式
#define MaxSize 10    //静态链表的最大长度
typedef int ElemType;
struct Node{
    ElemType data;
    int next;       //下一个元素的数组下标
}

void testSLinkList(){
    struct Node a[MaxSize];  //定义数组a作为静态链表
    //...
}


//实现方式二:结构体数组方式定义
#define MaxSize 10    //静态链表的最大长度
typedef int ElemType;
struct {
    ElemType data;
    int next;       //下一个元素的数组下标
}SLinkList[MaxSize];

void testSLinkList(){
    SLinkList a //定义数组a作为静态链表
    //...
}

基本操作的实现

查找: 从头结点出发挨个往后遍历结点,O(n)
插入位序为i的结点:
1.找到一个空的结点,存入数据元素
2. 从头结点出发找到位序为i-1的代码
3. 修改新结点的next
4. 修改i-1的结点的next
删除某个结点:

顺序表和链表的比较

逻辑结构

都属于线性表,都是线性结构

存储结构

顺序表:
优点:支持随机存取,存储密度高
缺点:大片连续空间分配不方便,改变容量不方便

链表:
优点:离散的小空间分配方便,改变容量方便
缺点:不可随机存取,存储密度低

基本操作

创消、增删改查
在这里插入图片描述
在这里插入图片描述在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值