线性结构(顺序表 链表 双指针法总结)

二、线性结构

2.1 线性表的定义

线性表是具有相同数据类型的 n ( n ≥ 0 ) n(n \geq 0) n(n0)个数据元素的有限序列,n为表长。 n = 0 n=0 n=0时线性表是一个空表。

若用L命名线性表,一般表示为: L = ( a 1 , a 2 , . . . , a i , a i + 1 , . . . , a n ) L = (a_1,a_2,...,a_i,a_{i+1},...,a_n) L=(a1,a2,...,ai,ai+1,...,an)

式中, a 1 a_1 a1是唯一的“第一个”数据元素,又称为“表头元素”; a n a_n an是唯一的“最后一个”数据元素,又称为“表尾元素”。

除第一个元素外,每个元素有且仅有一个直接前驱;除最后一个元素外,每个元素有且仅有一个直接后继。

线性表是一种逻辑结构,表示元素之间一对一的相邻关系。顺序表和链表是指存储结构,两者属于不同层面的概念。

线性表的特点:

  1. 表中元素个数有限。
  2. 表中元素有逻辑上的顺序性,表中元素有先后次序。
  3. 表中元素都是数据元素,每个元素都是单个元素。
  4. 表中元素的数据类型都相同,每个元素占有相同大小的存储空间。
  5. 表中元素具有抽象性,即仅讨论元素间的逻辑关系,而不考虑元素究竟表示什么内容。
2.2 顺序表
2.2.1 顺序表

用一组地址连续的存储单元依次存储线性表中的数据元素,从而使得逻辑上相邻的两个元素在物理上也相邻。

线性表中元素的位序是从1开始的,数组中元素的下标是从0开始的。

i i i是元素 a i a_i ai在线性表中的位序。

C代码描述:

//静态分配
#define MaxSize 50
typedef struct{
    ElemType data[MaxSize];
    int length;
}SqList;
//类型定义

//动态分配
#define InitSize 100
typedef struct{
    ElemType *data;
    int MaxSize,length;
}SeqList;

L.data = (ElemType *) malloc(sizeof(ElemType) * InitSize);

注:动态分配不是链式存储,同样属于顺序存储结构,物理结构没有变化。

依然是随机存取的方式,只是分配的空间大小可以在运行时确定。

基本操作的实现:

  1. 插入 - O ( n ) O(n) O(n)

    bool ListInsert(SqList &L,int i,ElemType e){
        if(i < 1 || i > L.length + 1) return 0;
        if(L.length >= MaxSize) return 0;
        
        for(int j = L.length;j >= i;--j) L.data[j] = L.data[j - 1];
        
        L.data[i - 1] = e;
        L.length++;
        return 1;
    }
    
  2. 删除 - O ( n ) O(n) O(n)

    bool ListDelete(SqList &L,int i,ElemType &e){
        //e中返回被删除元素
        if(i < 1 || i > L.length) return 0;
        e = L.data[i - 1];
        
        for(int j = i;j < L.length;++j) L.data[j - 1] = L.data[j];
        L.length--;
        return 1;
    }
    
  3. 按值查找 - O ( n ) O(n) O(n)

    int LocateElem(SqList &L,ElemType e){
        for(int i = 0;i < L.length;++i){
            if(L.data[i] == e)
                return i;
        }
        return 0;
    }
    
2.2.2 examples - 1

e.1

长度为n的非空线性表采用顺序存储结构,在表的第 i i i个位置插入一个元素,则 i i i的合法范围是: 1 ≤ i ≤ n + 1 1 \leq i \leq n+1 1in+1

n + 1 n+1 n+1表示在表尾追加元素。

e.2

已知在一维数组 S [ m + n ] S[m+n] S[m+n]中依次存放两个线性表 A ( a 1 , a 2 , . . . , a m ) A(a_1,a_2,...,a_m) A(a1,a2,...,am) B ( b 1 , b 2 , . . . , b n ) B(b_1,b_2,...,b_n) B(b1,b2,...,bn)。编写一个函数,将两个顺序表的位置互换,即将B放到A的前面。

思想:现将整个数组 S 逆置,此时得到 ( b n , b n − 1 , . . . , b 1 , a m , a m − 1 , . . . , a 1 ) (b_n,b_{n-1},...,b_1,a_m,a_{m-1},...,a_1) (bn,bn1,...,b1,am,am1,...,a1),再对前n个数和后m个数分别逆置,从而实现两个表的顺序互换。

代码:

void Reverse(ElemType S[],int left,int right,int arraySize){
    if(left >= right || right >= arraySize)return;
    int mid = (left + right) / 2;
    for(int i = 0;i <= mid - left;++i){
        ElemType temp = S[left + i];
        S[left + i] = S[right - i];
        S[right - i] = temp;
    }
}

void Exchange(ElemType S[],int m,int n,int arraySize){
    Reverse(S,0,m+n-1,arraySize);
    Reverse(S,0,n-1,arraySize);
    Reverse(S,n,m+n-1,arraySize);
}

e.3

长度为 L ( L ≥ 1 ) L(L\geq 1) L(L1)的升序序列 S S S,处在第 ⌈ L / 2 ⌉ \lceil L/2\rceil L/2个位置的数称为 S S S的中位数。现有两个等长升序序列 A A A B B B,设计一个算法找到两个序列的中位数。

思想:

分别求出两个序列 A , B A,B A,B的中位数 a , b a,b a,b,依次执行:

  1. a = b a=b a=b,则 a a a b b b即是所求中位数。

  2. a < b a<b a<b,则舍弃 A A A的较小的一半和 B B B的较大的一半,两段要相等。

  3. a > b a>b a>b,则舍弃 A A A的较大的一半和 B B B的较小的一半,两段要相等。

在保留的序列中重复计算中位数并重复过程123,直到两个序列均含有一个元素为止,此时两个元素中的较小者就是中位数。

代码:

int M_Search(int A[], int B[], int n){
    int l1, r1, a;
    int l2, r2, b;
    l1 = l2 = 0;
    r1 = r2 = n - 1;
    while(l1 != r1 || l2 != r2){ // 左闭右闭区间 相等即为单个元素
        a = A[(l1 + r1) / 2];
        b = B[(l2 + r2) / 2];
        if(a == b)return a;
        if(a < b){
            l1 = (l1 + r1) / 2;
            r2 = (l2 + r2) / 2;
            if((r1 - l1 + 1) % 2 == 0) l1++; // 长度为偶数
        } else {
            r1 = (l1 + r1) / 2;
            l2 = (l2 + r2) / 2;
            if((r2 - l2 + 1) % 2 == 0) l2++; // 长度为偶数
        }
    }
    if(A[l1] < B[l2]) return A[l1];
    return B[l2];
}

时间复杂度为 O ( l o g 2 n ) O(log_2n) O(log2n),空间复杂度为 O ( 1 ) O(1) O(1)

e.4

整数序列 A = ( a 0 , a 1 , . . . , a n − 1 ) A = (a_0,a_1,...,a_{n-1}) A=(a0,a1,...,an1),其中 0 ≤ a i < n   ( 0 ≤ i < n ) 0 \leq a_i < n\ (0\leq i <n) 0ai<n (0i<n),如果存在元素出现超过 n / 2 n/2 n/2次,那么称该元素为主元素。设计算法,找出 A A A的主元素,若存在,输出主元素,若不存在,输出 − 1 -1 1

思路:

  1. 依次扫描数组中的每个元素,将第一个整数 N u m Num Num存到 c c c中,记录 c n t cnt cnt 1 1 1
  2. 扫描接下来的元素,如果遇到的数仍为 N u m Num Num,就让 c n t + 1 cnt+1 cnt+1,否则 c n t − 1 cnt-1 cnt1 c n t cnt cnt减到 0 0 0时,将遇到的下一个整数存到 c c c中, c n t cnt cnt重新记为 1 1 1,开始新一轮计数。
  3. 重复2过程,直到扫描完全部数组元素。
  4. 判断 c c c中的元素是否真的是主元素。再次扫描数组,统计 c c c中的元素出现次数,若大于 n / 2 n/2 n/2,就是主元素;否则,序列中不存在主元素。

代码:

int Majority(int A[], int n){
    int c, cnt = 1;
    c = A[0];
    for(int i = 1;i < n;++i){
        if(A[i] == c) cnt++;
        else if(cnt > 0) cnt--;
        else {
            c = A[i];
            cnt = 1;
        }
    }
    
    if(cnt > 0){
        cnt = 0;
        for(int i = 0;i < n;++i){
            if(A[i] == c) cnt++;
        }
    }
    if(cnt > n / 2) return c;
    return -1;
}

时间复杂度 O ( n ) O(n) O(n),空间复杂度 O ( 1 ) O(1) O(1)

2.3 链式表示

链式存储线性表时,不需要使用地址连续的存储单元,即不要求逻辑上相邻的元素在物理位置上也相邻,通过“链”建立起数据元素之间的逻辑关系。插入和删除操作不需要移动元素,而只需要修改指针。但失去随机存取的优点。

链表的每个结点都带有指针域,故而存储密度不够大,存储密度小于1。

2.3.1 单链表

通过一组任意的存储单元来存储线性表中的数据元素。

对每个链表节点,除存放元素自身的信息外,还需存放一个后继的指针。

类型描述:

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

单链表可以解决顺序表需要大量连续存储空间的缺点,但单链表附加指针域,也存在浪费存储空间的缺点。

单链表元素离散地分布在存储空间中,所以单链表是非随机存取的存储结构,即不能直接找到表中某个特定的节点,查找某个特定的节点时,需要从表头开始遍历,依次查找。

通常用头指针来标识一个单链表,头指针为NULL时表示一个空表。

为了操作上的方便,在单链表第一个节点之前附加一个结点,称为头结点。

头结点的数据域可以不设任何信息,也可记录表长等信息;头结点的指针域指向线性表的第一个元素结点。

不管带不带头结点,头指针始终指向链表的第一个节点,头结点是带头结点的链表的第一个节点。

引入头结点的优点:

  1. 第一个数据结点的位置被存放在头结点的指针域中,所以在链表的第一个位置上的操作和在表的其他位置上的操作一致,无需特殊处理。
  2. 无论链表是否为空,其头指针都指向头结点的非空指针,因此空表和非空表的处理也就得到了统一。

(增设头结点的目的是方便运算的实现)

头插法建立单链表:

LinkList List_HeadInsert(LinkList L,int A[],int n){
    // 顺序相反 复杂度O(n)
    // 含有头结点
    LNode *s;
    L = (LinkList)malloc(sizeof LNode);
    L->next = NULL;
    for(int i = 0;i < n;++i){
        s = (LNode *)malloc(sizeof LNode);
        s->data = A[i];
        s->next = L->next;
        L->next = s;
    }
    return L;
}

尾插法建立单链表:

LinkList List_TailInsert(LinkList L,int A[],int n){
    // 顺序相同 复杂度O(n)
    // 需要设置一个尾指针r
    L = (LinkList)malloc(sizeof LNode);
    LNode *s, *r;
    r = L;
    for(int i = 0;i < n;++i){
        s = (LNode *)malloc(sizeof LNode);
        s->data = A[i];
        r->next = s;
        r = x;
    }
    r->next = NULL;
    return L;
}

按序号查找,按值查找:

// 复杂度均为O(n)

LNode *GetElem(LinkList L,int i){
    // 按序号查找
    if(i == 0) return L; // 返回头结点
    if(i < 1) return NULL; // i无效
    int cnt = 1; // 设置计数
    LNode *p = L->next;
    while(p && cnt < i){
        p = p->next;
        cnt++;
    }
    return p;
}


LNode *LocateElem(LinkList L,ElemType e){
    // 按值查找 不存在返回NULL
    LNode *p = L->next;
    while(p != NULL && p->data != e) p = p->next;
    return p;
}

插入操作:

bool Insert_Elem(LinkList L,int i,ElemType e){
    // 后插操作 先找前驱结点
    LNode *p = GetElem(L, i - 1);
    if(p == NULL) return 0;
    LNode *s;
    s->data = e;
    s->next = p->next;
    p->next = s;
    return 1;
}

bool Insert_Elem(LinkList L,int i,ElemType e){
    // 前插操作 先找前驱结点 交换前驱结点和当前插入结点存放的数据
    LNode *p = GetElem(L, i - 1);
    if(p == NULL) return 0;
    LNode *s;
    s->next = p->next;
    p->next = s;
    s->data = p->data;
    p->data = e;
    return 1;
}

删除操作:

bool Delete_Elem(LinkList L,int i){
    // 删除指定位置的元素
    LNode *p = GetElem(L, i - 1);
    if(p == NULL) return 0;
    LNode *s;
    s = p->next;
    p->next = s->next;
    free(s);
    return 1;
}

// 删除给定的结点
bool Delete_Elem(LNode *p){
    // 先将后继结点的值赋予自身 然后删除后继结点
    p->data = p->next->data;
    LNode *q = p->next;
    p->next = q->next;
    free(q);
}

求表长:

int GetLength(LinkList L){
    int cnt = 0;
    LNode *p = L->next;
    while(p != NULL){
        p = p->next;
        cnt++;
    }
    return cnt;
}
2.3.2 双链表

双链表有两个指针prior和next,分别指向其前驱结点和后继结点。

类型描述:

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

各个操作均与单链表类似,注意指针的变化情况。

2.3.3 循环链表
  1. 循环单链表

最后一个结点的指针不是NULL而改为指向头结点,从而整个链表形成一个环。

判空的条件不是头结点的指针是否为空,而是它是否等于头指针。

在任何一个位置插入都是等价的,无需判断表尾。

循环单链表可以从任意一个结点开始遍历整个表。

若操作主要是在表头表尾进行的,可以只设置尾指针,对表头表尾的操作均是 O ( 1 ) O(1) O(1)

  1. 循环双链表

头结点的prior指针还要指向尾结点。

循环双链表为空链表时,头结点的prior和next都等于L。

2.3.4 静态链表

借助数组来描述线性表的链式存储结构,结点也有data和next两个域,这里的next指的是结点的相对地址(数组下标),又称游标。

和顺序表一样,静态链表也需要预先分配一块连续的内存空间。

类型描述:

#define MaxSize 50
typedef struct LinkList{
    ElemType data;
    int next;
} SLinkList[MaxSize];

静态链表通常以next == -1为结束标志。

常用操作与动态链表的相同,只需要修改指针而不需要移动元素。

2.3.5 双指针法合集

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

补充:找到环的入口的方法。在快慢指针相遇点和起点处同时设置一个指针,两个指针每次走一步,相遇处即为环的入口点。
在这里插入图片描述

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1ZXYWesD-1610883173661)(image-20201018193441089.png)]

2.3.6 examples - 2

e.1

某线性表用带头结点的循环单链表存储,头指针是head,当head->next->next == head成立时,线性表的可能长度是

0 0 0 1 1 1

e.2

设计一个递归算法,删除不带头结点的单链表L中所有值为x的点。

递归实现,函数 f(L,x) 的功能是删除以L为首节点指针的单链表中所有值为x的节点。边界为L为空。

代码:

void Delete_X(Linklist L, ElemType x){
    // 使用引用,直接对原链表进行操作
    LNode *p;
    if(L == NULL)return;
    if(L->data == x){
        p = L;
        L = L->next;
        free(p);
        Delete_X(L, x);
    } else {
        Delete_X(L->next, x);
    }
}

e.3

设L为带头节点的单链表,试实现从尾到头反向输出每个结点的值。

递归输出较为方便。

代码:

void R_Print(LinkList L){
    if(L->next != NULL) R_Print(L->next);
    if(L != NULL) print(L->data);
}

e.4

给定两个单链表,编写算法找出两个链表的公共节点。

两个链表有公共节点,即两个链表从某一结点开始,它们的next都指向同一个节点。拓扑形状看起来像Y。

假设一个链表比另一个链表长k个结点,现在长的链表上遍历k个结点,然后再同步遍历,就能保证同时到达同一个节点。

时间复杂度O(len1 + len2)

代码:

LNode* Search_First_Common(LinkList L1, LinkList L2) {
    int len1 = Length(L1), len2 = Length(L2);
    
    int dis = 0;
    LNode *l1, *l2;
    // l1 表示较长的 l2 表示较短的
    if(len1 > len2){
        l1 = L1->next;
        l2 = L2->next;
        dis = len1 - len2;
    } else {
        l1 = L2->next;
        l2 = L1->next;
        dis = len2 - len1;
    }
    
    while(dis--) l1 = l1->next;
    while(l1 != NULL){
        if(l1 == l2) return l1;
        else {
            l1 = l1->next;
            l2 = l2->next;
        }
    }
    return NULL;
}

e.5

两个整数序列 A = a 1 , a 2 , . . . , a m A = a_1,a_2,...,a_m A=a1,a2,...,am B = b 1 , b 2 , . . . , b n B = b_1,b_2,...,b_n B=b1,b2,...,bn分别存入两个单链表中,设计一个算法,判断序列B是否是序列A的连续子序列。

朴素算法。操作从两个链表第一个结点开始,若对应数据相等,则后移指针,若不等,则A链表从上次开始比较结点的后继开始,B链表仍从第一个结点开始比较。

B链表到尾则表示匹配成功,A到尾而B没到尾则表示失败。

可优化。

代码:

int Pattern(LinkList A, LinkList B) {
    LNode *p = A, *q = B;
    LNode *pre = p;
    while(p && q){
        if(p->data == q->data){
            p = p->next;
            q = q->next;
        } else {
            pre = pre->next;
            p = pre;
            q = B;
        }
    }
    if(q == NULL) return 1;
    return 0;
}

e.6

设计一个算法,判断一个链表是否有环,如果有,找出环的入口并返回,否则返回NULL。

设置快慢两个指针 fast 和 slow ,初始时都指向链表头head。

fast 每次走两步,slow每次走一步。fast 和 slow 指定会相遇。

接下来继续双指针法找到环的入口。

代码:

LNode* FindLoopStart(LinkList L){
    LNode *fast, *slow;
    fast = L;
    slow = L;
    while(fast != NULL) {
        slow = slow->next;
        fast = fast->next;
        if(fast != NULL) fast = fast->next;
        if(slow == fast) break;
    }
    
    if(slow == NULL || fast->next == NULL) return 0;
    //没有环,走到了NULL
    
    LNode *p1 = L;
    LNode *p2 = slow;
    while(p1 != p2){
        p1 = p1->next;
        p2 = p2->next;
    }
    return p1;
}

e.7

设线性表 L = ( a 1 , a 2 , . . . , a n − 1 , a n ) L = (a_1,a_2,...,a_{n-1},a_n) L=(a1,a2,...,an1,an)采用带头节点的单链表保存,链表中结点定义如下

typedef struct node{
    int data;
    struct node *next;
}NODE;

设计一个算法,空间复杂度为 O ( 1 ) O(1) O(1),时间上尽可能高效,重新排列L中的各个结点,得到线性表 L ′ = ( a 1 , a n , a 2 , a n − 1 , a 3 , a n − 2 . . . ) L' = (a_1,a_n,a_2,a_{n-1},a_3,a_{n-2}...) L=(a1,an,a2,an1,a3,an2...)

双指针法,找到线性表的中间点。

后半段原地逆置。

前后两个半段依次选取节点,按要求重排。

代码:

void change_list(NODE* L){
    NODE *p, *q;
    p = L;
    q = L;
    while(q->next != NULL){
        p = p->next;
        q = q->next;
        if(q != NULL) q = q->next;
    }
    
    q = p->next;
    p->next = NULL;
    NODE *r;
    // 头插法逆置
    while(q != NULL){
        r = q->next;
        q->next = p->next;
        p->next = q;
        q = r;
    }
    
    NODE *s = L->next;
    q = p->next;
    p->next = NULL;
    while(q != NULL){
        r = q->next;
        q->next = s->next;
        s->next = q;
        s = q->next;
        q = r;
    }
}

2.4 顺序表和链表的比较
顺序表链表
存取(读写方式)可以顺序存取,也可随机存取。只能从表头开始顺序存取元素。
逻辑结构与物理结构逻辑上相邻的元素对应的物理存储位置也相邻逻辑上相邻元素物理存储位置不一定相邻;逻辑关系通过指针来表示
按值查找无序 O ( n ) O(n) O(n);有序 O ( l o g 2 n ) O(log_2n) O(log2n) O ( n ) O(n) O(n)
按序号查找 O ( 1 ) O(1) O(1) O ( n ) O(n) O(n)
插入、删除需要移动元素 O ( n ) O(n) O(n)只需修改相关结点的指针域 O ( n ) O(n) O(n)
空间分配预分配过大导致大量空间闲置;过小会发生溢出。动态分配需要移动大量元素,导致操作效率变低。只要有内存空间就可以分配,操作灵活、高效

如何选取存储结构

顺序表链表
基于存储考虑难以估计线性表长度或存储规模时
基于运算考虑常按序号访问元素、较稳定的线性表常插入、删除操作(虽然也要找位置,但主要是基于比较操作)
基于环境考虑任何高级语言中均有数组类型基于指针,实现较为复杂
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值