数据结构-第二章 线性表-笔记

目录

2011考研真题-求两个升序数组的中位数

2020考研真题-三元组中的最小距离

使用尾插法建立链表和头插法逆置链表

2.3 线性表的链表表示-课后习题

链表的初始化

1、递归算法,删除不带头结点的单链表L中所有值为X的结点

5、将带头结点的链表就地逆置

6、递增排序单链表

8、找出两个单链表的公共结点

10、将单链表A分解成两条单链表A和B,A中存放序号为奇数的元素,B中存放序号为偶数的元素

13、将两个递增排序的单链表,合并为一个递减排序的单链表

14、两个单链表A和B递增有序,要求用两个链表中的公共元素产生一条单链表,不破坏A和B中的结点

16、判断链表B的序列是否是链表A中的一个子序列

20、设计访问函数,保持链表频度递减

21、判断所给的单链表是否存在环

22、查找链表中倒数第k个位置上的结点(k为正整数)

25、重新排列单链表


2011考研真题-求两个升序数组的中位数

给定两个数组A和B,要求寻找两个数组的中位数,之前没做过这种类型的题目,一开始想的是O(n)的方法,没想到可以用二分查找缩小到O(nlogn)

参考自:求两个有序数组的中位数_kenby的专栏-CSDN博客_两个有序数组求中位数

我觉得这个方法比书上所给的要好理解不少

int find_midnum(int A[], int B[], int m, int n, int left, int right)
{
    //c表示有多少个数字比要寻找的中位数小,例如9个数的中位数是第5位,则有4个数比中位数小
    int c = (m + n - 1) / 2;
    int mid = (left + right) / 2;

    //如果在A数组中找不到,就换成在B数组中找,这个递归函数肯定能返回
    if (left > right)
    {
        return find_midnum(B, A, m, n, 0, n - 1);
    }
    //如果在A中有mid个数比A[mid]小,在B中只有c-mid-1个数比A[mid]小,说明A[mid]是中位数
    //这里的两个等号都是合理的,例如在10个数中,中位数是第5个数,那么第4个数和第6个数都跟第5个数相等也是没问题的
    if (B[c - mid - 1] <= A[mid] && A[mid] <= B[c - mid])
    {
        return A[mid];
    }
    //如果A[mid]太小了,就从A[mid]的前面部分中继续找
    else if (B[c - mid - 1] > A[mid])
    {
        return find_midnum(A, B, m, n, mid + 1, right);
    }
    //如果A[mid]太大了,就从A[mid]的后面部分中继续找
    else if (A[mid] > B[c - mid])
    {
        return find_midnum(A, B, m, n, left, mid - 1);
    }
}

int main()
{   
    int m, n;
    int A[] = { 1,3,5,7};
    int B[] = { 2,4,6,10,11};

    m = sizeof(A) / sizeof(int);
    n = sizeof(B) / sizeof(int);

    cout << find_midnum(A, B, m, n, 0, m - 1);  //输出6

    return 0;
}

2020考研真题-三元组中的最小距离

对我而言挺有难度的一题,真是没想出来,另外感觉题目的说法也不够严谨,这里的三个数组实际上任意一个都可以是S1、S2或者S3,也就是说a、b、c取自哪个数组是可以随时变化的。

这题的关键在于每次都放大一点最小值a,因为移动c会导致距离增大,而移动b要么导致距离不变,要么移动过多导致距离也增大。

代码如下:

int find_minlen(int A[], int n, int B[], int m, int C[], int p)
{
    int i = 0, j = 0, k = 0;
    int minlen = INT_MAX, len;
    while (i < n && j < m && k < p)
    {
        len = abs(A[i] - B[j]) + abs(B[j] - C[k]) + abs(A[i] - C[k]);
        minlen = min(minlen, len);

        //每次将最小值放大一点
        if (A[i] < B[j] && A[i] < C[k]) i++;
        else if (B[j] < A[i] && B[j] < C[k]) j++;
        else k++;
    }
    return minlen;
}

int main()
{   
    int n, m, p;
    int A[] = { -1, 0, 9};
    int B[] = { -25, -10, 10, 11};
    int C[] = { 2, 9, 17, 30, 41 };

    n = sizeof(A) / sizeof(int);
    m = sizeof(B) / sizeof(int);
    p = sizeof(C) / sizeof(int);

    cout << find_minlen(A, n, B, m, C, p);  //输出2,相应的三元组为(9,10,9)

    return 0;
}

使用尾插法建立链表和头插法逆置链表

看网课时,听老师提了一嘴可以用头插法逆置链表,以前没实现过但觉得还是挺简单的,结果实现起来却卡了一会壳,写出来的也不好看,下面是参考别人的写法:

struct node
{
    int data;
    node* next = nullptr;
};

int main()
{   
    int x;
    cin >> x;
    node* head = new node;
    node* rear = head;
    //输入1 2 3 4 5 
    while (x != 9999)
    {
        node* cur = new node;
        cur->data = x;
        rear->next = cur;
        rear = cur;
        cin >> x;
    }
    for (node* it = head->next; it != nullptr; it = it->next)
    {
        cout << it->data << " ";    //输出1 2 3 4 5 
    }cout << endl;

    node* cur, * temp;     
    //由cur领走原链表,让哨兵节点指向空,方便在哨兵节点后逆置链表
    cur = head->next;          
    head->next = nullptr;      
    while (cur != nullptr)
    {
        //将cur所指向的节点单独取出来,使用头插法创建新的链表
        temp = cur;
        cur = cur->next;
        temp->next = head->next;
        head->next = temp;
    }
    for (node* it = head->next; it != nullptr; it = it->next)
    {
        cout << it->data << " ";    //输出5 4 3 2 1 
    }
    return 0;
}

2.3 线性表的链表表示-课后习题

链表的初始化

单链表版

struct node
{
    int data = 0;
    node* next = nullptr;
};

node* initlist(node* head, vector<int> a)
{
    node* cur = head;
    for (int num : a)
    {
        node* temp = new node;
        temp->data = num;
        cur->next = temp;
        cur = cur->next;
    }
    return head;
}

int main()
{   
    node* head = new node;
    vector<int> a = { 2, 3, 5, 1, 0, 2, 9, 8 };
    head = initlist(head, a);

    head->next = delnode(2, head->next);

    node* it = head->next;
    while (it != nullptr)
    {
        cout << it->data << " ";
        it = it->next;
    }

    return 0;
}

双链表版 

struct node
{
    int data = 0;
    node* next = nullptr;
    node* prev = nullptr;
};

node* initlist(node* head, vector<int> a)
{
    node* cur = head;
    for (int num : a)
    {
        node* temp = new node;
        temp->data = num;
        cur->next = temp;
        temp->prev = cur;
        cur = cur->next;
    }
    cur->next = head;
    head->prev = cur;
    return head;
}

int main()
{   
    node* head = new node;
    vector<int> a = { 2, 3, 5, 1, 0, 2, 9, 8 };
    head = initlist(head, a);

    node* it = head->next;
    while (it != head)
    {
        cout << it->data << " ";
        it = it->next;
    }cout << endl;

    it = head->prev;
    while (it != head)
    {
        cout << it->data << " ";
        it = it->prev;
    }cout << endl;

    return 0;
}

1、递归算法,删除不带头结点的单链表L中所有值为X的结点

node* delnode(int x, node* cur)
{
    if (cur != nullptr)
    {
        if (cur->data == x)
        {
            node* temp = cur->next;
            delete cur;
            //删除当前结点,并将后续第一个data值不是x的结点或者nullptr连接到前驱结点
            return delnode(x, temp);
        }
        else
        {
            //如果cur的后续没有结点的话,依旧等于nullptr
            //如果cur的下一个非空结点的值不是x的话,依旧连接在cur后
            //如果cur的下一个非空结点的值是x的话,那么连接的是递归后,后续第一个data值不是x的结点或者nullptr
            cur->next = delnode(x, cur->next);
            return cur;
        }
    }
    return nullptr;
}

注意是不带头结点的,一开始审题错了,第一次传进去的应该是head->next。

5、将带头结点的链表就地逆置

void reverselist(node* head)
{
    node* left = head->next;
    if (left == nullptr) return;
    node* right = left -> next;
    left->next = nullptr;
    //每次逆置left所指向的结点,right用来判断left是不是链表的最后一个结点
    while (right != nullptr)
    {
        node* temp = left;
        left = right;
        right = right->next;
        left->next = temp;
    }
    //最终right所表示的是nullptr,left表示的是第一个结点,所以让头结点指向最后的left
    head->next = left;
}

写这种简单的代码还是会出现脑子短路的情况。。

6、递增排序单链表

void sortlist(node* head)
{
    bool flag = true;
    while (flag)
    {
        flag = false;
        node* pre = head;
        node* left = head->next;
        if (left == nullptr) return;
        node* right = left->next;
        while (right != nullptr)
        {
            if (left->data > right->data)
            {
                flag = true;
                pre->next = right;
                left->next = right->next;
                right->next = left;
                right = left->next;
                pre = pre->next;
            }
            else
            {
                pre = pre->next;
                left = left->next;
                right = right->next;
            }
        }
    }
}

额排序除了sort(),就只会一个冒泡了

8、找出两个单链表的公共结点

node* commomnode(node* headA, node* headB)
{
    node* nodea = headA;
    node* nodeb = headB;
    //如果链表没有交点,那么两个指针最后依旧会同时到达nullptr,并退出循环
    while (nodea != nodeb)
    {
        nodea = nodea == nullptr ? headB : nodea->next;
        nodeb = nodeb == nullptr ? headA : nodeb->next;
    }
    return nodea;
}

但是在力扣上写过:2021.9.16 力扣-链表相交_作用太大了销夜的博客-CSDN博客

官方所给的这种巧妙的方法又一次令我惊叹,不仅体现在思想上的巧妙,而且代码写的也十分的漂亮,只要用一次循环即可。

10、将单链表A分解成两条单链表A和B,A中存放序号为奇数的元素,B中存放序号为偶数的元素

void turntwolist(node* headA, node* headB)
{
    node* left = headA;
    node* right = headA->next;
    node* cur = headB;
    int i = 1;
    while (right != nullptr)
    {
        //将A中序号为偶数的结点放到B中,同时从A中删去
        if (i % 2 == 0)
        {
            node* temp = right;
            right = right->next;
            left->next = right;
            cur->next = temp;
            cur = temp;
        }
        else
        {
            left = left->next;
            right = right->next;
        }
        i++;
    }
}

13、将两个递增排序的单链表,合并为一个递减排序的单链表

node* combinelist(node* head1, node* head2)
{
    node* ans = new node;
    node* p1 = head1->next;
    node* p2 = head2->next;
    while (p1 != nullptr && p2 != nullptr)
    {
        //头插法,将较小的先放入ans链表中
        if (p1->data <= p2->data)
        {
            node* temp = p1;
            p1 = p1->next;
            temp->next = ans->next;
            ans->next = temp;
        }
        else
        {
            node* temp = p2;
            p2 = p2->next;
            temp->next = ans->next;
            ans->next = temp;
        }
    }
    //解决某条链表还有剩余元素的情况
    while (p1 != nullptr)
    {
        node* temp = p1;
        p1 = p1->next;
        temp->next = ans->next;
        ans->next = temp;
    }
    while (p2 != nullptr)
    {
        node* temp = p2;
        p2 = p2->next;
        temp->next = ans->next;
        ans->next = temp;
    }
    return ans;
}

跟将两个有序数组合并为一个有序数组类似。

注意这里对于每条链表只要使用一个指针即可,而第10题对于A链表必须使用两个指针,因为第10题要求在A链表中删去若干个结点之后还要返回A链表,因此若删去了结点right,那么它的前驱结点left的next指针必须指向后续的结点,否则就断链了;但是这题由于返回的是一条新的链表,因此不用管原来的两条链表是否断链了。

总结一下就是,对于删除结点的操作,如果删除后链表还需要使用则需要使用两个指针;如果删除后链表不再使用,则只需要一个指针。

14、两个单链表A和B递增有序,要求用两个链表中的公共元素产生一条单链表,不破坏A和B中的结点

node* commonnodelist(node* headA, node* headB)
{
    node* ans = new node;
    node* pa = headA->next;
    node* pb = headB->next;
    while (pa != nullptr && pb != nullptr)
    {
        if (pa->data < pb->data)
        {
            pa = pa->next;
        }
        else if (pa->data > pb->data)
        {
            pb = pb->next;
        }
        else
        {
            node* temp = new node;
            temp->data = pa->data;
            temp->next = ans->next;
            ans->next = temp;
            pa = pa->next;
            pb = pb->next;
        }
    }
    return ans;
}

16、判断链表B的序列是否是链表A中的一个子序列

bool ifsubsequence(node* headA, node* headB)
{
    //pre是正常在链表A上走的指针
    node* pre = headA->next;
    //pa和pb分别用来比较A和B中对应的两个结点元素是否相等
    node* pa = headA->next;
    node* pb = headB->next;
    while (pa && pb)
    {
        //如果当前pa和pb对应的两个结点元素相等,就继续往后比较
        if (pa->data == pb->data)
        {
            pa = pa->next;
            pb = pb->next;
        }
        //否则,pre向后走一步,pa重新等于pre,pb重新回到链表B的第一个结点
        else
        {
            pre = pre->next;
            pa = pre;
            pb = headB->next;
        }
    }
    //若最终pb等于nullptr,说明完整地遍历了一遍链表B,B是A的一个子序列
    if (pb == nullptr) return true;
    return false;
}

20、设计访问函数,保持链表频度递减

L是为带有头结点的非循环双向链表,每个结点中还有一个访问频度域freq,相关结点每被访问一次频度加一,设计一个Locate(L, x)函数,访问data值为x的结点,并保持链表中频度递减排序,频度相同的情况下最近访问的结点排在前面,返回找到的结点

node* Locate(node* L, int x)
{
    node* p = L->next;
    while (p != nullptr)
    {
        if (p->data == x)
        {
            p->freq++;
            //当p是链表的第一个结点,或者p的频度小于前面结点的频度时,可以直接返回p
            if (p == L->next || p->freq < p->prev->freq) return p;
            //先将当前这个结点提取出来,并从链表中去掉
            node* temp = p;
            p = p->next;
            temp->prev->next = p;
            p->prev = temp->prev;
            
            node* pre = temp->prev;
            //寻找插入位置
            while (pre != L && pre->freq <= temp->freq)
            {
                pre = pre->prev;
            }
            temp->next = pre->next;
            pre->next->prev = temp;
            pre->next = temp;
            temp->prev = pre;
            return temp;
        }
        p = p->next;
    }
}

看着题目挺复杂,实际上逻辑和实现都挺简单的

21、判断所给的单链表是否存在环

    bool hasCycle(node *head) {
        node* fast = head->next, * slow = head->next;
        while (fast != nullptr && slow != nullptr)
        {
            fast = fast->next;
            if (fast != nullptr) fast = fast->next;
            else break;
            slow = slow->next;
            if (fast == slow) return true;
        }
        return false;
    }

在力扣上写过:2021.9.17 力扣-环形链表_作用太大了销夜的博客-CSDN博客

这里用的是快慢指针的方法。

22、查找链表中倒数第k个位置上的结点(k为正整数)

int Locate(node* head, int k)
{
    node* left = head, * right = head;
    for (int i = 0; i < k; i++)
    {
        if (right != nullptr) right = right->next;
        else return 0;
    }
    while (right != nullptr)
    {
        left = left->next;
        right = right->next;
    }
    cout << left->data << endl;;
    return 1;
}

想着这不是两次遍历就能解决了吗,看了下答案,用两次遍历的话是拿不了满分的TT。

一次遍历就是用双指针法。

25、重新排列单链表

将单链表a1, a2, a3......an-1,an 重新排列为 a1,an,a2,an-1,a3......,要求空间复杂度为O(1)

void rearrange(node* head)
{
    node* p1 = head, * p2 = head;
    //该循环得到的p1是原链表的中间结点,也是最终所需的链表的最后一个结点
    //得到的p2是原链表的最后一个结点
    while (p2->next != nullptr)
    {
        p1 = p1->next;
        p2 = p2->next;
        if (p2->next != nullptr) p2 = p2->next;
        else break;
    }

    //将p2设为p1的下一个结点,即后半段链表的第一个结点
    p2 = p1->next;
    //这里是个妙招,先将p1->next改为nullptr,可以方便地让逆置后的链表的尾结点指向nullptr
    p1->next = nullptr;
    //用p1->next和p2来逆置后半段链表
    //在这个过程中p1位置始终不变
    //该循环得到的p1->next是逆置后的后半段链表的第一个结点
    while (p2 != nullptr)
    {
        node* temp = p2->next;
        p2->next = p1->next;
        p1->next = p2;
        p2 = temp;
    }

    //left是前半段链表的第一个结点,right是逆置后的后半段链表的第一个结点
    node* left = head->next;
    node* right = p1->next;
    //p1一直是原链表的中间结点,也是最终所需的链表的最后一个结点,于是可以将p1->next赋为nullptr
    p1->next = nullptr;
    //将逆置后的后半段链表中的结点,逐个插入到前半段链表中
    while (right != nullptr)
    {
        node* temp = right->next;
        right->next = left->next;
        left->next = right;
        left = right->next;
        right = temp;
    }
}

这题感觉好难啊,实在花费了大量的时间,一开始就没有想到正确的思路上去,在看完答案后自己写的过程中又被一些临界情况搞晕了,最后还是一句一句地研究答案中的代码才看懂了。

首先就是如何找到中间结点的问题,在草稿纸上试了一下发现,在结点个数大于1时,不论是奇数还是偶数个结点,总有两种情况下重新排列后两种链表的尾节点是一样的,且是原链表的中间结点,例如8个结点和9个结点的链表,在重新排列后尾节点都是a5。因此找到这个中间结点至关重要,我原本想的是先遍历一次计算一下链表的长度,然后再遍历第二次找到,没想到忘了经典的双指针法,可见双指针在链表这块还是能发挥很大的作用的。

另外,这题有个巧妙的地方就是在找到中间结点p1之后,然后再让p1->next = nullptr,在逆置后半段链表的过程中p1一直留在原位不动,用p1->next和p2来逆置链表。由于一开始让p1->next = nullptr,所以后续在逆置的过程中只要一次简单的循环就可以让逆置后的后半段链表的尾结点指向nullptr了。

    //将p2设为p1的下一个结点,即后半段链表的第一个结点
    p2 = p1->next;
    //这里是个妙招,先将p1->next改为nullptr,可以方便地让逆置后的链表的尾结点指向nullptr
    p1->next = nullptr;

而如果按正常思路,用两个结点来逆置链表的话,由于结点类型没法赋值为nullptr,所以在处理尾结点的指向时,还需要额外判断,不太方便。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值