【数据结构】线性表

  • 引入头结点:①统一第一个元素结点与其他结点的操作;②统一空表和非空表的处理。

顺序表

数组有上界和下界,数组的元素在上下界内是连续的。

  • 存储10,20,30,40,50的数组的示意图如下:

  • 数组的特点是:数据是连续的;随机访问速度快。
    数组中稍微复杂一点的是多维数组和动态数组。对于C语言而言,多维数组本质上也是通过一维数组实现的。至于动态数组,是指数组的容量能动态增长的数组;对于C语言而言,若要提供动态数组,需要手动实现;而对于C++而言,STL提供了Vector;对于Java而言,Collection集合中提供了ArrayList和Vector。

链表

  • 一般地插入删除:都需要前一个结点。

题目:一个链表最常用的操作是在末尾插入结点(即 需要尾结点)和删除结点(即 需要尾的前驱),则选用“带头结点的双循环链表”最省时间。

题目:设对n个元素的线性表的运算只有4种:删除第一个元素(第一的前);删除最后一个元素(尾的前);在第一个元素之前插入新元素(第一的前);在最后一个元素之后插入新元素(),则最好使用“只有头结点指针没有尾结点指针的循环双链表”(这个链表满足前面所述所有条件)。

单向链表

单向链表(单链表)是链表的一种,它由节点组成,每个节点都包含下一个节点的指针。

  • 单链表的示意图如下:

    表头为空,表头的后继节点是"节点10"(数据为10的节点),"节点10"的后继节点是"节点20"(数据为10的节点),...

  • 单链表删除节点

    删除"节点30"
    删除之前:"节点20" 的后继节点为"节点30",而"节点30" 的后继节点为"节点40"。
    删除之后:"节点20" 的后继节点为"节点40"。

//删除p之后的s
p->next = s->next;
free(s);
  • 单链表插入节点

    在"节点10"与"节点20"之间添加"节点15"
    添加之前:"节点10" 的后继节点为"节点20"。
    添加之后:"节点10" 的后继节点为"节点15",而"节点15" 的后继节点为"节点20"。
//在p之后插入s,确保不断链
s->next = p->next;
p->next = s;
  • 扩展:对某一节点进行前插操作
    先后插,再交换前后数据,可等价为前插
//将s结点插入到p之前
s->next = p->next;
p->next = s;
swap(s->data,p->data);
  • 扩展:删除结点*p
    将其后继结点的值赋予其自身,再删除p的后继结点
q = p->next;
p->data = p->next->data;
p->next = q->next;
free(q);
  • 单链表的特点是:节点的链接方向是单向的;相对于数组来说,单链表的的随机访问速度较慢,但是单链表删除/添加数据的效率很高。

双向链表

双向链表(双链表)是链表的一种。和单链表一样,双链表也是由节点组成,它的每个数据结点中都有两个指针,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点。一般我们都构造双向循环链表。

  • 双链表的示意图如下:

    表头为空,表头的后继节点为"节点10"(数据为10的节点);"节点10"的后继节点是"节点20"(数据为10的节点),"节点20"的前继节点是"节点10";"节点20"的后继节点是"节点30","节点30"的前继节点是"节点20";...;末尾节点的后继节点是表头。

  • 双链表删除节点:

    删除"节点30"
    删除之前:"节点20"的后继节点为"节点30","节点30" 的前继节点为"节点20"。"节点30"的后继节点为"节点40","节点40" 的前继节点为"节点30"。
    删除之后:"节点20"的后继节点为"节点40","节点40" 的前继节点为"节点20"。
//删除p之后的s
p->next = s->next;
s->next->prior = p;
  • 双链表插入节点:

    在"节点10"与"节点20"之间添加"节点15"
    添加之前:"节点10"的后继节点为"节点20","节点20" 的前继节点为"节点10"。
    添加之后:"节点10"的后继节点为"节点15","节点15" 的前继节点为"节点10"。"节点15"的后继节点为"节点20","节点20" 的前继节点为"节点15"。
//在p之后插入s,确保不断链
s->prior = p;
s->next = p->next;
s->prior->next = s;
s->next->prior = s;

静态链表

静态链表借助数组来描述线性表的链式存储结构,结点也有数据域data和指针域next,与前面的链表的指针不同的是,这里的指针是结点的相对地址数组下标),又称游标。和顺序表一样,静态链表也要预先分配一块连续的内存空间。

  • 存储结构:
# define MaxSize 50 //静态链表的最大长度
typedef struct {    //静态链表结构类型的定义
    ElemType data;  //存储数据元素
    int next;       //下一个元素的数组下标
}SLinkList[MaxSize];//Static
  • 静态链表以next==-1作为其结束的标志。
  • 静态链表的插入删除操作与动态链表的相同,只需要修改指针,而不需要移动元素。

    总体来说,静态链表没有单链表使用起来方便,但在一些不支持指针的高级语言(如 Basic)中,这是一种非常巧妙的设计方法。

顺序表

注意:数组只是顺序表的实体表现,而不是就是顺序表。这是一个抽象和一个实体的关系。

  • 存储结构:
#define MaxSize 50          //定义线性表的最大长度
typedef struct {
    ElemType data[MaxSize]; //顺序表的元素
    int length;             //顺序表的当前长度
}SqList;                    //顺序表的类型定义

常用算法

放过法

问题:对长度为n的顺序表L,编写一个时间复杂度为O(n)、空间复杂度为O(1)的算法,该算法删除线性表中所有值为x的数据元素。

  • 算法:
bool deleteX(SqList &L, ElemType x) {
    int k = 0;      // 记录值不等于x的元素个数
    for(int i=0; i<L.length; i++) {
        if(L.data[i] != x) {        // 如果不等于x,那就放你过,让你进入线性表(新)
            L.data[k] = L.data[i];
            k++;
        }
    }
    L.length = k;
}

逆置

  • 算法:
//顺数第i个+倒数第i个=总长度n-1
void reverse(SqList &L) {
    ElemType t;
    for(int i = 0; i<L.length/2; i++) {     // 依次交换前后元素
        t = L.data[i];
        L.data[i] = L.data[length-i-1];
        L.data[length-i-1] = t;
    }
}

归并算法

链表

  • 存储结构:
typedef struct LNode {      //定义单链表结点类型
    ElemType data;          //数据域
    struct LNode *next;     //指针域
}LNode, *LinkList;

常用算法

放过法

其实链表无需放过法,其插入和删除元素很方便,不需要移动元素。

问题:
在一个递增有序的线性表中,有数值相同的元素存在。若存储方式为单链表,设计算法去掉数值相同的元素,使表中不再有重复的元素。

  • 设计思想:
    由于是有序表,所有相同值域的结点都是相邻的,用p扫描递增单链表L,
    若*p结点的值域等于其后继结点的值域,则删除后者,否则p移向下一个结点。
  • 算法:
void deleteSame(LinkList &L) {
    LNode *p = L->next, *q;     //q是操作指针用来删除
    if(p == NULL) {
        return;
    }
    while(p->next != NULL) {
        if(p->data == p->next->data) {
            q = p->next;        //赋值给q,删除p->next
            p->next = q->next;  
            free(q);
        }else {
            p = p->next;    //否则前进
        }
    }
}
  • 效率:
    时间复杂度:O(n)
    空间复杂度:O(1)

头插法(逆置)

生成后进先出的单链表(链式栈)

  • 逆序(逆置):
q->next = head->next;
head->next = q;

尾插法

  • 顺序:
q->next = rear->next;
rear->next = q;
rear = q;   //尾指针继续指向尾结点

归并算法

两个已经有序的线性表,归并为一个有序的线性表。(两两比较取较小值放入新的有序线性表

问题:已知两个链表A和B分别表示两个集合,其元素递增序列。编制函数,求A与B的交集,并存放于A链表中。

  • 设计思想:
    采用归并的思想,设置两个工作指针pa和pb,对两个链表进行归并扫描,只有同时出现在两集合中的元素才链接到结果表中且进保留一个,其他的结点全部释放。当一个遍历完毕后,释放另一个表剩下的全部结点。
  • 算法:
LinkList same(LinkList &A, LinkList &B) {
    LNode *pa = A->next, *pb = B->next, *q, *t = A;     //q为操作指针,t为尾指针
    A->next = NULL;     //摘下头结点
    while(pa && pb) {
        if(pa->data == pb->data) {
            q = pa;
            pa = pa->next;
            q->next = t->next;      //将A集合那个元素插入到结果表中(尾插法)
            t->next = q;
            t = q;          //尾插法必备
            
            q = pb;         //将B集合中相同的元素释放
            pb = pb->next;
            free(q);        //这两个集合,每扫描一个元素就释放一个,所以每次都是释放第一个元素,无需前驱后继链接
        }else if(pa->data < pb->data) {     //如果pa中的元素值比较小,那么后移指针,找大一点的(这样才能与pb匹配)
            q = pa;
            pa = pa->next;
            free(u);
        }else {
            q = pb;
            pb = pb->next;
            free(q);
        }
    }
    //如果还有链表非空,释放剩下的部分
    if(pb) {
        pa = pb;
    }
    while(pa) {
        q = pa;
        pa = pa->next;
        free(q);
    }
    free(B);        //释放B表头结点
    return A;
}
  • 效率:
    时间复杂度:O(len1+len2) //while(pa && pb)
    空间复杂度:O(1)

标尺法

找倒数第k个元素

  • 设计思想:
    MY:扫描链表所有结点,得到链表长度n;再从头扫描第n-k+1个结点后,即是倒数第k个,输出。
  • 算法:
typedef int ElemType;
typedef struct LNode {
    ElemType data;
    struct LNode *link;
}LNode, *LinkList;
    
int searchK1(LinkList list, int k) {
    LNode *p = list->link;
    int length = 0;
    int count = 0;
    while(p) {      //得到表长
        length++;
        p = p->link;
    }
    if(k<1 || k>length) {   //合法性检验
        return 0;
    }
    
    p = list->link;
    while(p) {          //寻找倒数第k个
        count++;
        if(count == length-k+1) {
            printf("%d", p->data);
            return 1;
        }else {
            p = p->link;
        }
    }
}
  • 标尺法:
int searchK2(LinkList list, int k) {
    LNode *p = list->link, *q = list->link;
    int count = 0;
    while(p) {      //找到第k个结点q(标尺长度为k)
        count++;
        if(count == k) {
            break;
        }else {
            p = p->link;
        }
    }
    if(!p) {        //如果p不存在,即p遍历完了链表,没找到第k个
        return 0;
    }
    while(p->next) {        //同步后移 (走到头,由于标尺长度为k,所以q就是倒数第k个)
        p = p->link;
        q = q->link;
    }
    printf("%d", q->data);
    return 1;
    
}
  • 效率:
    时间复杂度:O(n)
    空间复杂度:O(1)

同起共进法(同一起点共同前进)(共同后缀)

(同起共进法)共同后缀

问题:str1和str2两个链表,在后面一部分指向了同一条子链表,是公共的,要求出共同后缀。

  • 设计思想:
    1. 分别求出str1和str2所指的两个链表长度m和n。
    2. 将两个链表以表尾对齐:令指针p、q分别指向str1和str2的头结点,
      若m≥n,则指针p先走,使p指向链表中的第m-n+1个结点;(m-n=长度差,又因为从0开始,所以-1
      若m<n,则使q指向链表中的第n-m+1个结点,即 (使指针p和q所指的结点到表尾的长度相等)。
    3. 反复将指针p和q同步向后移动,当p、q指向同一位置时停止,即为共同后缀的起始位置,算法结束。
  • 算法:
typedef struct Node {
    char data;
    struct Node *next;
}SNode;
    //求链表长度
int listlen(SNode *head) {
    int len = 0;
    while(head->next!=NULL) {
        len++;
        head = head->next;
    }
    return len;
}

//找出共同后缀的起始地址(所以不能改变链表),不然地址变了
SNode *find_addr(SNode *str1, SNode *str2) {
    int m, n, i;
    SNode *p, *q;
    m = listlen(str1);
    n = listlen(str2);
    for(i = 0, p=str1; i<m-n+1; i++) {      //找到第m-n+1个结点   //或者i = 1, p=str1; i<=m-n+1; i++
        p = p->next;
    }
    for(i = 0, q=str2; i<n-m+1; i++) {
        q = q->next;
    }
    while(p->next!=NULL && p->next!=q->next) {
        p = p->next;
        q = q->next;
    }
    return p->next;
}
  • 效率:
    时间复杂度:O(len1+len2),其中len1、len2分别为两个链表的长度
    空间复杂度:O(1)

归纳总结

  • 对于链表,经常采用的方法有:头插法尾插法逆置法归并法双指针法等,对具体问题需要灵活变通;
  • 对于顺序表,由于可以直接存取,经常结合排序和查找的几种算法设计思路进行设计,如 归并排序二分查找等。

本文部分参考:https://www.cnblogs.com/skywang12345/p/3561803.html

原文地址:https://www.cnblogs.com/blknemo/p/11378992.html