0.基本结构和函数
前置内容,可以访问数据结构-单链表基础1
点击查看代码
typedef int ElemType;
typedef struct LNode {ElemType data;struct LNode *next;
} LNode, *linkList;
void CreateList_R(linkList *L, int n) {*L = (linkList) malloc(sizeof(LNode));(*L)->next = NULL;linkList lastp = *L;for (int i = 0; i < n; i++) {LNode *p = (LNode *) malloc(sizeof(LNode));scanf("%d", &(p->data));p->next = NULL;lastp->next = p;lastp = p;}
}
void PrintList(linkList L) {linkList p = L->next;while (p) {printf("%d ", p->data);p = p->next;}printf("\n");
}
1.单链表的逆置
给定一个单链表,要求空间复杂度O(1)实现逆置
分析
在单链表建立时,利用头插法可以实现逆序输入,原因是每次输入的结点“都挤进了头结点后面的位置”,我们利用这个思路,遍历单链表,每次都对结点使用头插法,就可以实现逆置
时间复杂度O(n),空间复杂度O(1)
代码具体实现
点击查看代码
void reverse(linkList *L) {linkList p = (*L)->next;(*L)->next = NULL;while (p) {LNode *q = p->next;p->next = (*L)->next;(*L)->next = p;p = q;}
}
2.单链表的最大值查找
利用单链表表示一个整数序列,在单链表中确定最大值
分析
该问题很简单,一次遍历比较即可
时间复杂度为O(n) 空间复杂度为O(1)
代码具体实现
点击查看代码
int findMax(linkList L) {linkList p = L->next;int max = p->data;while (p){max = max > p->data ? max : p->data;p = p->next;}return max;
}
3.链表的正负分解
给定la表示一个非零整数序列,把链表la分解为两个链表lb和lc,其中lb表的结点为la表中值小于零的结点,而lc表的结点为la表中值大于零的结点。要求空间复杂度为O(1)。
分析
这道题目很简单,遍历链表la后对于正负不同的元素分别使用尾插法插入lb和lc即可
时间复杂度为O(n) 空间复杂度为O(1)
代码具体实现
点击查看代码
void fun(linkList *La,linkList *Lb,linkList *Lc) {*Lb = (linkList) malloc(sizeof(LNode));*Lc = (linkList) malloc(sizeof(LNode));linkList pa = (*La)->next,pb = *Lb,pc = *Lc;while (pa) {linkList temp = pa->next;pa->next = NULL;if (pa->data < 0) {pb->next = pa;pb = pa;} else {pc->next = pa;pc = pa;}pa = temp;}pb->next = NULL;pc->next = NULL;
}
4.单链表的区间删除
给定一个有序链表和最小值和最大值,要求删除该链表中在区间[最小值,最大值]的所有结点,如果一个结点都没删或者全删了认为“删除失败”,不保证最小值最大值合法
分析
首先题目不保证合法,还定义了“删除失败”,所以我们的函数需要返回是否合法且删除成功。在c语言中我们的函数需要使用int类型(或者自定义bool类型)。对于[min,max]区间内的元素删除即可
时间复杂度为O(n) 空间复杂度为O(1)
代码具体实现
点击查看代码
int Delete(linkList *L,int min,int max,int n) {if(min>=max) return 0;linkList p = (*L)->next;linkList lastp = *L;int cnt = 0;while (p) {if(p->data >= min && p->data <= max) {lastp->next = p->next;cnt++;}elselastp = p;p = p->next;}if(cnt == n || cnt == 0) return 0;else return 1;
}
5.两个有序链表的交集
单链表la和lb分别示两个递增的整数集合,求出la和lb的交集并以同样的形式存储,并存放在la中。若无交集,输出0。要求空间复杂度为O(1)
分析
首先题目说了是有序集合,所以没有重复元素,我们使用“快慢指针”法,对于每一个la的结点,值为e,我们使用只需要找到lb中第一个不小于e的结点,比较此结点的值是否为e即可,如果不是就删除,这样做看起来复杂度是O(n^2)的,但是请注意两个链表均有序,所以lb不需要每次从头遍历,故最终时间复杂度为O(max(lena,lenb)),如果遍历结束la的指针还没有结束,需要赋值null代表后面的元素均不符合条件
时间复杂度为O(n) 空间复杂度为O(1)
代码具体实现
点击查看代码
int fun(linkList *La,linkList *Lb) {linkList pa = (*La)->next,pb = (*Lb)->next,pre = *La;pre->next = NULL;int cnt = 0;while(pa){while(pb && pb->data < pa->data)pb = pb->next;//找到第一个不小于此值的结点if(pb && pb->data == pa->data) {cnt ++;pre -> next = pa;pre = pa;//记录上一个符合条件的结点pa = pa->next;pb = pb->next;}else{linkList p = pa;pre -> next = pa -> next;pa = pa -> next;free(p);}}pre -> next = NULL;return cnt;
}
6.两个有序链表的差集
和上一题目类似,所求集合从 la ∩ lb 变成 la - lb
分析
不难发现,这道题目的思考逻辑和上一题完全是相对的,我们在求解两个有序集合的交集时,遇见满足条件(在la中且lb中第一个不小于的元素和其相等)的元素就加入,否则就删除。不难发现 la - lb = la - (la ∩ lb) ,所以我们在上一题的逻辑下,仅需要调换删除和加入的顺序就能实现,即满足条件就删除,不满足条件就加入,此处所说的条件和前文一致。代码实现上需要注意一些小细节,已经在注释中说明
时间复杂度为O(n) 空间复杂度为O(1)
代码具体实现
点击查看代码
int fun(linkList *La,linkList *Lb) {linkList pa = (*La)->next,pb = (*Lb)->next,pre = *La;pre->next = NULL;int cnt = 0;while(pa){while(pb && pb->data < pa->data) pb = pb->next;//读者可以与上一题代码进行比较,仅仅调换了位置if(pb && pb->data == pa->data) {linkList p = pa;pre -> next = pa -> next;pa = pa -> next;free(p);}else{cnt ++;pre -> next = pa;pre = pa;pa = pa->next;//pb = pb->next;/*略微不同的是pb指针不需要后移因为此时pa指向的元素小于pb指向的元素,pa后移后,元素有可能与pb指向的元素相等也有可能仍然小于,所以如果pb后移则会缺少情况*/}}pre -> next = NULL;return cnt;
}
7.单链表的排序
给定单链表,要求给出排序后的单链表
a.冒泡排序
分析
排序,是算法/数据结构的基础,可以说很多高阶数据结构都是基于排序的,对于学习过c语言的读者,必然会冒泡排序算法,这里不再赘述原理,直接给出代码
时间复杂度为O(n^2) 空间复杂度为O(1)
代码具体实现
点击查看代码
void bubble(linkList L) {if (L->next == NULL || L->next->next == NULL) return;linkList p, q, tail = NULL;int isswap; // 是否发生交换do {isswap = 0; p = L->next; // 从第一个节点开始q = p->next; // 第二个结点开始// 遍历到tail节点while (q != tail) {if (p->data > q->data) {ElemType temp = p->data;p->data = q->data;q->data = temp;isswap = 1; // 标记发生了交换}// 移动到下一对节点p = q;q = q->next;}// 更新尾指针 下一次不需要处理已排好的尾部tail = p;} while (isswap);
}
b.归并排序
分析
当然,O(n^2)的排序仅仅能应付校内一些基础的任务,在大部分情况下是无法满足我们的性能需求的,我们需要思考更高效的算法。在上一篇文章中,我介绍了两个有序链表的合并,即归并操作,我们可以利用归并来实现高效的排序。我们需要知道基本基本事实:单个元素一定是有序的 请想象一下,我们比较每两个元素,对其归并,这时候长度为n的链表变成了n/2个有序的长度为2的链表,然后我们再对相邻的长度为2的链表进行归并......以此类推,最终由2个长度为n/2归并,形成了所需要的有序链表。但是我们在代码实现的时候,实现逻辑是反过来的,因为我们不可能先将链表分成n份,然后再分成n/2份。我们反过来利用分治的思想对于排序的算法f(L),我们现把他分成两份,然后分别排序f(L1),f(L2),这样做函数就会递归调用,直到遇到边界情况,即长度为1的情况,此时退出递归。不难发现其时间复杂度由 “分”和“合” 两个阶段共同决定。
1.“分” 阶段:拆分链表
过程:通过快慢指针找到链表的中间节点,将链表拆分为两个子链表(L1和L2),递归处理子链表。
时间开销:每次拆分时,快慢指针遍历半个链表(长度为n/2),时间复杂度为O(n)。
递归结构:对于长度为n的链表,会拆分为两个长度为n/2的子链表,以此类推,直到子链表长度为 1。递归深度为log₂n(二分次数)。
2.“合” 阶段:合并有序链表
过程:Merge函数将两个有序子链表(L1和L2)合并为一个有序链表(L3),通过比较节点值依次链接。
时间开销:合并两个总长度为n的链表时,需要遍历所有节点一次,时间复杂度为O(n)。
递归结构:每层递归的总合并时间为O(n)(所有子链表合并的总长度为n),共log₂n层。
3.总时间复杂度
分阶段总时间:O(n) + O(n/2) + O(n/4) + ... + O(1) = O(n)(等比数列求和)。
合阶段总时间:O(n) × log₂n = O(nlogn)。
整体时间复杂度:O(nlogn)(分阶段时间被合阶段主导)。
时间复杂度为O(nlogn) 空间复杂度为O(logn) 此处空间复杂度为递归调用函数导致
代码具体实现
点击查看代码
void Merge(linkList L1, linkList L2, linkList *L3) {*L3 = (linkList) malloc(sizeof(LNode));(*L3)->next = NULL;linkList p1 = L1->next, p2 = L2->next, lastp = *L3;while (p1 && p2) {if (p1->data < p2->data)lastp->next = p1, p1 = p1->next;elselastp->next = p2, p2 = p2->next;lastp = lastp->next;}lastp->next = (p1) ? p1 : p2;
}void sort(linkList L) {if (L->next == NULL || L->next->next == NULL) return;linkList L1 = (linkList) malloc(sizeof(LNode)), L2 = (linkList) malloc(sizeof(LNode));L1->next = NULL, L2->next = NULL;linkList slow = L->next, fast = L->next->next; // 快慢指针,找中间值,因为链表不能下标访问data[n/2]while (fast && fast->next) slow = slow->next, fast = fast->next->next;L1->next = L->next, L2->next = slow->next, slow->next = NULL;sort(L1), sort(L2);linkList L3;Merge(L1, L2, &L3);L->next = L3->next;free(L1), free(L2), free(L3);
}