数据结构与算法 - 链表实战

链表只是一种数据结构,如果要通过数据结构来解决问题那就是算法了,所以这篇文章我们看看如何利用链表的数据结构去解决一些问题。 以下的代码均可 前往下载

合并有序链表

将2个递增的有序链表合并为一个有序链表; 要求结果链表仍然使用两个链表的存储空间,不另外占用其他的存储空间. 表中不允许有重复的数据

其实题目理解起来很简单 ,如果 La = {1,2,3,,6,9} ,Lb = {2,3,4,5,7,10} 那么合并后应该是Lc ={1,2,3,4,6,7,9,10},其规定不另外占用其他存储空间,那么我们就只能使用a,b这两个存储空间做手脚。

用 pa 和 pb 分别指向 La 和 Lb ,从首元节点开始比较,让Lc指向其中较小的值,并让较小的值依次后移,直到其中一个链表循环完毕,那么如果还有多余的数据,一定是比较大的,直接接在Lc后面即可:

Status MergeList(LinkList *La,LinkList *Lb,LinkList *Lc) {
    // 找到首元节点,依次遍历
    Node *pa = (*La)->next;
    Node *pb = (*Lb)->next;
    // 由于不能开辟新的空间,我们借用La的空间
    Node *pc = *La;
    // pc是一个局部变量 保存的是Lc的尾节点,每次都指向两个值中的最小值,如果相等则保持其一,删除释放另外一个
    while (pa && pb) {
        if (pa->data < pb->data) {
            pc->next = pa;
            pc = pa;// 这地方相当于pc = pc->next 也就是说pc指针也要后移
            pa = pa->next;
        } else if (pa->data > pb->data) {
            pc->next = pb;
            pc = pb;
            pb = pb->next;
        } else {
            Node *temp = pb;
            pc->next = pa;
            pc = pa;
            pa = pa->next;
            pb = pb->next;
            free(temp);
        }
    }

    // 最后多余的数据一定是后续最大的
    pc->next = pa?pa:pb;
    *Lc = *La;
    return SUCCESS;
}

验证

int main(int argc, const char * argv[]) {
    // insert code here...
    printf("Hello, World!\n");
    // 算法题1
    LinkList la,lb,lc;
    InitList(&la);
    InitList(&lb);
    InitList(&lc);
    for (int i = 6; i > 0; i --) {
        ListInsert(&la, 1, i);
    }
    printf("打印la :\n");
    ListTraverse(la);

    for (int i = 10; i > 2; i-=2) {
        ListInsert(&lb, 1, i);
    }
    printf("打印lb :\n");
    ListTraverse(lb);

    MergeList(&la, &lb, &lc);
    printf("打印lc :\n");
    ListTraverse(lc);
    return 0;
}
-----------------------------打印结果
Hello, World!
打印la :
1  2  3  4  5  6  
打印lb :
4  6  8  10  
打印lc :
1  2  3  4  5  6  8  10  
Program ended with exit code: 0

求交集

已知两个链表A和B分别表示两个集合.其元素递增排列. 设计一个算法,用于求出A与B的交集,并存储在A链表中;

例如: La = {2,4,6,8}; Lb = {4,6,8,10}; => Lc = {4,6,8}

其实思路跟上面那道题差不多,也是用两个指针从前往后依次遍历,具体看代码,内部有详细注释

Status Intersection(LinkList *La,LinkList *Lb,LinkList *Lc) {
    Node *pa = (*La)->next;
    Node *pb = (*Lb)->next;
    Node *pc = *La;
    Node *temp;
    while (pa && pb) {
        // 每次碰到小的就过滤掉并释放
        if (pa->data < pb->data) {
            temp = pa;
            pa = pa->next;
            free(temp);
        } else if (pa->data > pb->data) {
            temp = pb;
            pb = pb->next;
            free(temp);
        } else {
            // 碰到相等的保留其中一个并释放另外一个
            pc->next = pa;
            pc = pc->next;
            pa = pa->next;
            temp = pb;
            pb = pb->next;
            free(temp);
        }
    }
    // 要额外判断多余的情况
    while (pa) {
        temp = pa;
        pa = pa->next;
        free(temp);
    }

    while (pb) {
        temp = pb;
        pb = pb->next;
        free(temp);
    }
    *Lc = *La;
    return SUCCESS;
}

验证

int main(int argc, const char * argv[]) {
    LinkList la,lb,lc;
    InitList(&la);
    InitList(&lb);
    InitList(&lc);
    for (int i = 6; i > 0; i --) {
        ListInsert(&la, 1, i);
    }
    printf("打印la :\n");
    ListTraverse(la);

    for (int i = 10; i > 2; i-=2) {
        ListInsert(&lb, 1, i);
    }
    printf("打印lb :\n");
    ListTraverse(lb);

    Intersection(&la, &lb, &lc);
    printf("打印lc :\n");
    ListTraverse(lc);
    return 0;
}
--------------------------- 打印结果
打印la :
1  2  3  4  5  6  
打印lb :
4  6  8  10  
打印 lc :
4  6  

链表倒序

设计一个算法,将链表中所有节点的链接方向”原地旋转”,即要求仅仅利用原表的存储空间. 换句话说,要求算法空间复杂度为O(1);

这里的空间复杂度 O(1) 其实也是一样,不允许新的辅助空间,所以我们还是要在原有的链表基础上做手脚。

之前的文章中我们讲过链表的初始化有两种方式:头插法尾插法 ,最后我们发现尾插法跟我们日常逻辑差不多, 而头插法却是倒序的:因为先插入的结点为表尾,后插入的结点为表头,即可实现链表的逆转;:

void Inverse(LinkList *L) {
    //  头插法每次要插入的节点,初始是首元节点
    Node *target = (*L)->next;
    Node *tNext;
    // 为了让链表的尾结点指向NULL
    (*L)->next = NULL;
    while (target) {
        // 每次让tNext保存target之后的数据
        tNext = target->next;
        target->next = (*L)->next;
        (*L)->next = target;
        target = tNext;
    }
}

验证

int main(int argc, const char * argv[]) {
    LinkList la,lb,lc;
    InitList(&la);
    InitList(&lb);
    InitList(&lc);
    for (int i = 6; i > 0; i --) {
        ListInsert(&la, 1, i);
    }
    printf("打印la :\n");
    ListTraverse(la);

    Inverse(&la);
    printf("打印la :\n");
    ListTraverse(la);
    return 0;
}
------------------------------------打印结果
打印la :
1  2  3  4  5  6  
打印la :
6  5  4  3  2  1  
Program ended with exit code: 0

删除指定范围的节点

设计一个算法,删除递增有序链表中值大于等于mink且小于等于maxk(mink,maxk是给定的两个参数,其值可以和表中的元素相同,也可以不同)的所有元素

其实思路很简单,找到左边最后一个小于mink的节点,以及找到右边第一个大于maxk的节点。

void DeleteDataRange(LinkList *L, int min,int max) {
    Node *p = (*L)->next;
    Node *left = *L,*right=NULL;
    // 找到左边节点
    while (p && p->data < min) {
        left = p;
        p = p->next;
    }

    // 找到右边节点
    while (p && p->data <= max) {
        right = p;
        p = p->next;
    }
    right = right->next;

    // 左边节点指向右边节点
    left->next = right;

    // 临时保存要删除的节点进行释放
    Node *temp = left->next;
    while (temp && temp != right) {
        Node *del = temp;
        temp = temp->next;
        free(del);
    }
}

验证

int main(int argc, const char * argv[]) {
    LinkList la,lb,lc;
    InitList(&la);
    InitList(&lb);
    InitList(&lc);
    for (int i = 6; i > 0; i --) {
        ListInsert(&la, 1, i);
    }
    printf("打印la :\n");
    ListTraverse(la);

    DeleteDataRange(&la, 23);
    printf("打印la :\n");
    ListTraverse(la);
    return 0;
}
---------------------打印结果
打印la :
1  2  3  4  5  6  
打印la :
1  4  5  6  

调整次序

设将n(n>1)个整数存放到一维数组R中, 试设计一个在时间和空间两方面都尽可能高效的算法;将R中保存的序列循环左移p个位置(0<p<n)个位置, 即将R中的数据由(x0,x1,……,xn-1)变换为(xp,xp+1,…,xn-1,x0,x1,…,xp-1)

阿西吧!,这段描述完全看不懂有没有,其实简化一下是这样:

 例如:
 pre[10] = {0,1,2,3,4,5,6,7,8,9},
 n = 10,p = 3;
 pre[10] = {3,4,5,6,7,8,9,0,1,2}

数组循环往左移动多少位,左边位置固定,超出的移到数组后面。注意:有的同学可能会想我用两个指针指向链表的两个位置进行重新排列就好了啊,这里题目中要求是数组而不是链表,所以不能用链表的方法去解决问题。

// 将数组指定范围的数据进行倒序
void Reverse (int *array,int left,int right ) {
    int i = left,j = right ;
    // 首位对调
    int temp;
    while (i < j) {
        temp = array[i];
        array[i] = array[j];
        array[j] = temp;
        // i右移  j左移
        i++;
        j--;
    }
}
//将长度为n的数组pre 中的数据循环左移p个位置
void LeftShift(int *pre,int n,int p){
    // 比如 {1,2,3,4,5} 向左移2位
    if (p > 0 && p < n) {
        // 整个数组数据全部倒序 {5,4,3,2,1}
        Reverse(pre, 0, n-1);
        // 前部分倒序 {3,4,5,2,1}
        Reverse(pre, 0, n-p-1);
        // 后部分倒序 {3,4,5,1,2}
        Reverse(pre, n-p, n-1);
    }
}

其实就是反复利用倒序的函数对数组进行调整,这里就不做验证了,请自行去验证一下即可。

找元素

已知一个整数序列A = (a0,a1,a2,…an-1),其中(0<= ai <=n),(0<= i<=n). 若存在ap1= ap2 = …= apm = x,且m>n/2(0<=pk<n,1<=k<=m),则称x 为 A的主元素. 例如,A = (0,5,5,3,5,7,5,5),则5是主元素; 若B = (0,5,5,3,5,1,5,7),则A 中没有主元素,假设A中的n个元素保存在一个一维数组中,请设计一个尽可能高效的算法,找出数组元素中的主元素,若存在主元素则输出该元素,否则输出-1.

其实有点像消消乐的算法,找到当前符合规定的元素,如果存在输出(消消乐消除),不存在返回-1。题目中提示尽可能高效,意思就是优先满足时间复杂度,所以我们可以考虑用空间换时间。

题目分析: 主元素,是数组中的出现次数超过一半的元素; 当数组中存在主元素时,所有非主元素的个数和必少于一半. 如果让主元素和一个非主元素配对, 则最后多出来的元素(没有元素与之匹配就是主元素.

int MainElement(int *A,int n) {
    int count = 1;
    int key = A[0];

    for (int i = 1; i < n; i ++) {
        int temp = A[i];
        if (temp == key) {
            count ++;
        } else {
            if (count > 0) {
                count --;
            } else {
                key = temp;
                count = 1;
            }
        }
    }

    int keyNum = 0;
    if (count > 0) {
        for (int i = 0; i < n; i ++) {
            if (A[i] == key) {
                keyNum++;
            }
        }
    }

    return keyNum > n/2 ? key:-1;
}

链表去重

用单链表保存m个整数, 结点的结构为(data,link),且|data|<=n(n为正整数). 现在要去设计一个时间复杂度尽可能高效的算法. 对于链表中的data 绝对值相等的结点, 仅保留第一次出现的结点,而删除其余绝对值相等的结点.例如,链表A = {21,-15,15,-7,15}, 删除后的链表A={21,-15,-7}

题目分析:

要求设计一个时间复杂度尽量高效的算法,而已知|data|<=n, 所以可以考虑用空间换时间的方法. 申请一个空间大小为n+1(0号单元不使用)的辅助数组. 保存链表中已出现的数值,通过对链表进行一趟扫描来完成删除.

void DeleteEqualNode(LinkList *L,int n) {
    int *p = malloc(sizeof(int)*n);
    for (int i = 0; i < n; i ++) {
        *(p+i) = 0;
    }
    // r 指向头节点
    Node *r = *L;
    // 首元节点
    Node *temp = (*L)->next;
    while (temp) {
        ListData absData = abs(temp->data);
        if (p[absData] == 1) {// 说明已经有值了
            r->next = temp->next;
            free(temp);
        } else {
            p[absData] = 1;
            r = temp;
        }
        temp = temp->next;
    }
}

其实相当于把数字的绝对值作为数组下标保存起来,内部的值只有0和1,这样就可以遍历的过程中直接取值进行比较,速度会非常快,无非就是初始化的时候需要一个大小为n的数组空间

总结

以上就是最近学习的链表相关的知识运用以及相关的问题解答,后续有相关的题目还会陆续更新。