第二章线性表——王道课后习题代码
自用整理,有错误欢迎指正!!
参考博主Mancuoj
一,顺序表
顺序表结构体
考试直接使用,一般不会让你写出结构体
#define ElemType int
#define MaxSize 50
typedef struct {
ElemType data[MaxSize];
int length;
}SqList;
2.2.3.1
- 暴力解法一般就是循环,循环一次不行,就来两次,两次不行三次 …
- 时间复杂度 O(n),空间复杂度O(1)
// 删除最小值函数
int Del_Min(SqList &L)
{
if (L.length < 1) // 当顺序表为空时,终止操作
{
exit(0);
}
int flag = 0; // 记录最小数值的数组下标
for (int i = 0; i < L.length; ++i)
{
if (L.data[i] < L.data[flag])
{
flag = i;
}
}
int min = L.data[flag]; // 保存最小数值
L.data[flag] = L.data[L.length - 1]; // 将顺序表末尾值复制到最小数值位置
L.length -= 1; // 顺序表长度减一
return min; // 返回最小值
}
2.2.3.2
- 暴力就是再整一个数组,原数组从末尾循环遍历放到新数组里
- 要求空间复杂度O(1)的话,从头循环到中间,交换头尾元素
- 偶数或者奇数个元素,循环到中间,都是 < length/2,因为如果长度为4或者5,都是循环到下标为1即停,为5的时候最中间的元素(下标为2)不需要移动。
- 时间复杂度O(n)
// 算法思想:扫描顺序表L的前一半元素,用i记录表首开始的元素下标,
// 通过临时变量temp交换L.data[i]与L.data[L.length-i-1]
// 反转函数
void Reverse(SqList &L)
{
int i;
ElemType temp;
for (i = 0; i < L.length / 2; ++i)
{
temp = L.data[i];
L.data[i] = L.data[L.length - i - 1];
L.data[L.length - i - 1] = temp;
}
}
2.2.3.3
- 暴力就是再整一个数组,循环遍历把不等于x的元素都放入新数组,但是空间复杂度为O(n),不符合要求
- 时间复杂度O(n),空间复杂度O(1)就要求一次循环内解决
- 把所有等于x的元素都扔到最后,然后 length 减掉就可以了
- 也就是把所有不等于x的元素扔到前面,后面自然就是等于x的元素了
// 算法思想:用k记录顺序表L中不等于x的元素的个数(即需要保存的元素个数)
// 扫描时将不等于x的元素往前移动(移动到下标k的位置)
//扫描后修改顺序表长度为k
void Delete_1(SqList &L, ElemType x)
{
int k = 0;
for (int i = 0; i < L.length; ++i)
{
if (L.data[i] != x)
{
L.data[k++] = L.data[i];
}
}
L.length = k;
}
// 算法思想:用k记录顺序表L中等于x的元素个数
// 当扫描到不等x时将L.data[i]的元素前移k个位置
// 最后修改顺序表的长度 L.length=L.length-k
void Delete_2(SqList &L, ElemType x)
{
int i;
int k = 0; // 记录顺序表中元素不等于x的个数
for (i = 0; i < L.length; ++i)
{
if (L.data[i] == x)
{
k++;
}
else
{
L.data[i - k] = L.data[i]; // 当前元素前移k个位置
}
}
L.length -= k;
}
- 一次循环 -> 双指针
- 类似快排,从两端向中间移动,将左边的x与右边的非x交换
- 自己的代码思路
// 算法思想:在顺序表中设置头尾两个指针i=0 j=L.length-1;
// 从两端移动,在遇到最左端值为x时,将最右端非x的元素移到x的数据所在的位置,直到j>i(遍历完所有的元素)
void Delete_3(SqList &L, ElemType x)
{
int i = 0;
int j = L.length - 1;
while (i <= j)//遍历所有的元素
{
if (L.data[i] == x)//顺序表左端遇到为x的元素,寻找右端不为x的元素进行替换
{
L.length--;
while (L.data[j] == x && j > i)//寻找右端不为x的元素
{
j--;
L.length--;
}
L.data[i] = L.data[j];//元素进行替换
j--;
}
i++;
}
}
- 优秀博主的思路
// 太麻烦了,不如上一个方法
void del_x_2(SqList &list, int x)
{
int i = -1, j = list.length, k = 0; // k用于记录x的个数
while (i < j)
{
while (list.data[++i] != x); // 寻找顺序表左端为x的元素
while (list.data[--j] == x)//寻找顺序表右端不为x的元素
k++;
if (i < j)
{
swap(list.data[i], list.data[j]);//两元素进行交换
k++;
}
}
list.length -= k;//更新顺序表的长度
}
2.2.3.4
- 跟第3题一样的解法
- 时间复杂度O(n),空间复杂度O(1)
void del_st(SqList &list, int s, int t) {
if (s >= t || list.length == 0) {
cout << "ERROR!" << endl;
return;
}
// 1.要保存的值都放在前面
int k = 0;
for (int i = 0; i < list.length; i++) {
if (list.data[i] < s || list.data[i] > t) {
list.data[k++] = list.data[i];
}
}
// 2.直接扔掉后面的值
list.length = k;
}
- 王道答案:自找麻烦的做法
- 有序 -> 从头循环找 >= s,继续找 >t 的元素
- 一次循环,或者找t从后往前再循环一次
- while + ++ 真的非常好用,最好像我这样:先-1然后用前++
- 时间复杂度O(n),空间复杂度O(1)
void del_st2(SqList &list, int s, int t) {
if (s >= t || list.length == 0) {
cout << "ERROR!" << endl;
return;
}
// 1.找到大于等于s的值
int i = -1;
while (list.data[++i] < s);
// 2.如果全部元素均小于s
if (i >= list.length) {
cout << "ERROR!" << endl;
return;
}
// 3.找到大于t的元素
int j = i-1;
while (list.data[++j] <= t);
// 4.前移,直接占住被删元素的位置
while(j < list.length) {
list.data[i++] = list.data[j++];
}
list.length = i;
}
2.2.3. 5
- 与第4题只差了不是有序表,但3,4题的解法仍然可以用
- 只要把要保存的值放在前面,再扔掉后面的值就可以了
- 不要被答案限制了你的思路❌
- 时间复杂度O(n),空间复杂度O(1)
void del_st(SqList &list, int s, int t) {
if (s >= t || list.length == 0) {
cout << "ERROR!" << endl;
return;
}
// 1.要保存的值都放在前面
int k = 0;
for (int i = 0; i < list.length; i++) {
if (list.data[i] < s || list.data[i] > t) {
list.data[k++] = list.data[i];
}
}
// 2.直接扔掉后面的值
list.length = k;
}
- 和王道的算法思路是一样的,但代码更加简洁明了。
2.2.3. 6
- 自己的算法思路
// 算法思想:用变量k记录需要删除的重复元素
// 依次比较相邻的两个元素,若相同,则k++
// 若不相等,则把当前元素向前移动k的位置
// 最后一个元素,直接向前移动k个位置
void Delete_same(SqList &L)
{
int k = 0; // 记录需要删除的重复元素
for (int i = 0; i < L.length; ++i)
{
if (L.data[i] != L.data[i + 1] || i == L.length - 1)
{
L.data[i - k] = L.data[i];
}
else
{
k++;
}
}
L.length -= k;
}
- 有序列表 ➡️ 相同元素排列在一起
- 暴力,新开一个数组,将不同元素存入
- 需要两个指针分别操作两个数组
- 时间复杂度O(n),空间复杂度O(n)
void del_same(SqList &list) {
if (list.length == 0) return;
// 1.新开一个数组
SqList copied = list;
copied.data[0] = list.data[0];
// 2.把不同元素存入
int k = 0;
for (int i = 1; i < list.length; i++) {
if (list.data[k] != copied.data[i]) {
copied.data[++k] = list.data[i];
}
}
// 3.新换旧
copied.length = k + 1;
list = copied;
}
- 仔细想想,其实并不需要两个数组,双指针就可以
- 前一个存储,后一个判断
- 算法思想:默认第一个元素为非重复元素,指针从第二个元素后向后遍历,找到不同的元素,将其依次往前移动
bool del_same(SqList &L) {
if (L.length == 0) return false;
int k = 0;//k存储第一个不相同的元素,i为工作指针
for (int i = 1; i < L.length; i++) {
if (L.data[k] != L.data[i])//查找下一个值与上一个值不相等的元素
{
L.data[++k] = L.data[i];找到后将元素前移
}
}
L.length = k + 1;
return true;
}
2.2.3. 7⭐
- 这种题太典型了,合并有序顺序表以及合并有序链表,建议全文背诵hh
- 循环,取下两个之中的较小的放入结果表中
- 最后那个表还有剩余就把剩下的部分全部加入结果表
bool Merge(SqList A, SqList B, SqList &C)
{
// 将顺序表A与顺序表合并成一个新的顺序表C(不舍弃重复元素)
if (A.length + B.length > MaxSize) // 大于顺序表的最大长度
{
return false;
}
int i = 0, j = 0, k = 0;
while (i < A.length && j < B.length) // 循环,两两比较,小的存取顺序表C
{
if (A.data[i] <= B.data[j])
{
C.data[k++] = A.data[i++];
}
else
{
C.data[k++] = B.data[j++];
}
}
while (i < A.length) // 还剩一个没有比较完的顺序表,剩余部分全部插入C末尾
{
C.data[k++] = A.data[i++];
}
while (j < B.length)
{
C.data[k++] = B.data[j++];
}
C.length = k;
return true;
}
- 补充一下归并排序
void merge_sort(int l, int r) {
if (l >= r) return;
int mid = (l+r) >> 1;
merge_sort(l, mid);
merge_sort(mid+1, r);
int k = 0, i = l, j = mid+1;
while (i <= mid && j <= r) {
if (q[i] <= q[j])
tmp[k++] = q[i++];
else
tmp[k++] = q[j++];
}
while (i <= mid)
tmp[k++] = q[i++];
while (j <= r)
tmp[k++] = q[j++];
for (i = l, j = 0; i <= r; i++, j++)
q[i++] = tmp[j++];
}
2.2.3. 8
- 暴力方法,再开一个数组,然后就是循环!
- 一次循环不行,两次!
- 参数 m 和 n 为两个线性表的长度
- 时间复杂度O(m+n),空间复杂度O(m+n)
void change(SqList &list, int m, int n) {
// 前一个线性表[0, m-1], 后一个[m, m+n-1]
SqList copied = list;
int k = -1;
for (int i = m; i < m+n; i++) {
copied.data[++k] = list.data[i];
}
for (int i = 0; i < m; i++) {
copied.data[++k] = list.data[i];
}
list = copied;
}
- 当然还有一种四两拨千斤的方法,可以先逆置整个数组,再分别逆置其中的两个线性表
- 比如说[1, 2, 3, 4]变成[3,4 1, 2],先将[1,2]逆置成[2,1],再将[3,4]逆置成[4,3],最后将[2,1,4,3]逆置成[3,4,1,2]
- 时间复杂度O(m+n),空间复杂度O(1)
// 翻转函数:abc ->cba
void Reverse(SqList &L, int from, int to)
{
ElemType temp;
for (int i = 0; i < (to - from + 1) / 2; ++i)
{
temp = L.data[i + from];
L.data[i + from] = L.data[to - i];
L.data[to - i] = temp;
}
}
// 左循环函数L
void Change(SqList &L, int m, int n)
{
Reverse(L, 0, m - 1);
Reverse(L, m, m + n - 1);
Reverse(L, 0, m + n - 1);
}
void InitSq(SqList &L, int n)
{
ElemType x;
for (int i = 0; i < n; ++i)
{
cin >> x;
L.data[i] = x;
}
L.length = n;
}
2.2.3. 9
- 递增有序,暴力循环
- 需要考虑很多边界条件,容易出错
- 时间复杂度O(n),空间复杂度O(1)
void find_x2(SqList &list, int x) {
// 1.二分找x
int low = 0, high = list.length - 1, mid;
while (low <= high) {
mid = (low + high) / 2;
if (list.data[mid] == x) break;
else if (list.data[mid] < x) low = mid + 1;
else high = mid - 1;
}
// 2.找到了
if (list.data[mid] == x && mid != list.length - 1) {
swap(list.data[mid], list.data[mid + 1]);
return;
}
// 3.没找到, 此时low>high
list.length++;
int i = list.length - 2;
while (i > high) {
list.data[i + 1] = list.data[i];
i--;
}
list.data[i + 1] = x;
}
- 优化,使用二分查找,不需要特别记录i的值
- 当查找失败时,high 会记录到最后一个小于x的元素
- 时间复杂度O(logn),空间复杂度O(1)
// 算法思想:用折半查找算法去查找x的值,查找到,则交换与后一个元素的值。
// 没有找到就在合适的位置插入x,保持顺序表的有序性
void SearchExchangeInsert(SqList &L, ElemType x)
{
int low = 0, high = L.length - 1, mid; // low,high指向顺序表下界和上界
while (low <= high)
{
mid = (low + high) / 2; // 找中间位置
if (L.data[mid] == x) // 找到x,推出while循环
break;
else if (L.data[mid] < x) // 到中点mid的右半部分去查
low = mid + 1;
else // 到中点mid的左半部分去查
high = mid - 1;
}
if (L.data[mid] == x && mid != L.length - 1) // 若最后一个元素与x相等,则不存在语气后继交换操作
{
int temp = L.data[mid];
L.data[mid] = L.data[mid + 1];
L.data[mid + 1] = temp;
}
if (low > high) // 查找失败,插入数据x
{
int i;
for (i = L.length - 1; i > high; i--) // 后移元素
{
L.data[i + 1] = L.data[i];
}
L.data[i + 1] = x; // 插入元素
L.length++; // 更新顺序表长度
}
}
2.2.3. 10
- 这个真题跟第8题基本一样,我们可以直接copy过来改一下参数
- 首先就是暴力求解,新开一个数组,分别复制进去(把结构体改成数组是一样的)
- 这样的时间复杂度是O(n),空间复杂度也是O(n)
- 为了节省空间,也可以创建一个大小为p的数组暂存前一个数组[0, p-1],原数组整体左移后再依次放回,这样的空间复杂度会降到O§
// 算法思想:首先,将R数组中前p个元素复制到新创建的数组r中。
// 然后,从R数组第p个元素(下标为p-1)开始依次向前移动p个位置。
// 最后,将r数组中的元素依次复制到R[p]-R[n-1]中
void change(int R[], int n, int p)
{
int r[p]; // 创建临时存放数组r
// 将R数组中前p个元素复制到新创建的数组r中。
for (int i = 0; i < p; ++i)
{
r[i] = R[i];
}
// 从R数组第p个元素(下标为p-1)开始依次向前移动p个位置。
for (int j = p; j < n; ++j)
{
R[j - p] = R[j];
}
// 将r数组中的元素依次复制到R[p]-R[n-1]中
for (int k = 0; k < p; ++k)
{
R[k + n - p] = r[k];
}
}
- 四两拨千斤,整体逆置,再分别逆置
- 比如说[1, 2, 3, 4]逆置成[4, 3, 2, 1],然后其中一个数组[4, 3]逆置成[3, 4],另一个[2, 1]逆置成[1, 2],最后结果就是[3, 4, 1, 2]
- 时间复杂度O(n),空间复杂度O(1)
// 翻转函数:abc ->cba
void Reverse(int R[], int from, int to)
{
int temp;
for (int i = 0; i < (to - from + 1) / 2; ++i)
{
temp = R[i + from];
R[i + from] = R[to - i];
R[to - i] = temp;
}
}
// 左循环函数
void Change(int R[], int n, int p)
{
Reverse(R, 0, p - 1);
Reverse(R, p, n - 1);
Reverse(R, 0, n - 1);
}
2.2.3.11
- 找到两个有序序列合并后的中位数,那暴力解就直接合并呗
- 可以把第7题直接抄过来,然后返回 (A.length + B.length)/2 位置上的数即可
- 然后你会发现并不需要合并全部,也不需要一个辅助表来存储数据,只需要循环到这个位置就可以了
- 注意题目中要求,mid 应该等于 (A.length + B.length +1) / 2
- 时间复杂度O(n),空间复杂度O(1)
int merge(SqList A, SqList B) {
int i = 0, j = 0, k = -1, mid = (A.length + B.length + 1) / 2;
// 条件也不需要,因为我们找到中间值就会直接return
while (1) {
++k;
if (A.data[i] <= B.data[j]) {
if (k == mid-1) return A.data[i];
i++;
} else {
if (k == mid-1) return B.data[j];
j++;
}
}
}
- 也可以直接循环到 mid 处,不需要每次都判断一次 ==mid
int M_Search(SqList A, SqList B)
{
int i = 0, j = 0, mid = (A.length + B.length + 1) / 2; // mid 中位数向上取整 mid表示第mid个元素是中位数
while (i + j <= mid - 2) // mid-2表示中位数前一个元素的数组下标
{
if (A.data[i] <= B.data[j])
i++;
else
j++;
}
return A.data[i] < B.data[j] ? A.data[i] : B.data[j]; // 第mid个元素即为中位数
}
- 考试不建议最优解,时间成本太高(大佬除外),毕竟暴力解最多好像只有5分差距,没必要
- 我这里就不写了,可以看一下王道答案
2.2.3.12
- 找重复元素,且该元素个数要超过数组的一半,所以一个数组最多只有一个主元素
- 暴力,就是拿空间换时间,类似桶排序
- 开一个新数组,新数组的下标对应元素值,值对应元素个数(因为数的范围是确定的)
- 时间复杂度O(n),空间复杂度O(n)
// 算法思想:新开辟一个全0数组,
// 遍历旧数组,旧数组的值对应新数组的下标。
// 用新数组记录数组下标对应旧数组值的个数
// 遍历新数组找出主元素
int Find_main(SqList L)
{
// 1.初始化一个全为0的数组
ElemType A[L.length] = {0};
// 2.新数组的下标对应元素的值,值对应该元素的个数
for (int i = 0; i < L.length; ++i)
{
A[L.data[i]]++;
}
// 3.遍历找出主元素
for (int j = 0; j < L.length; ++j)
{
if (A[j] > L.length / 2)
{
return j;
}
}
// 4.没有找到返回-1
return -1;
}
- 第二种方法:我们先排好序再找主元素
- 记录每个元素出现的次数,如果后面的数不是重复元素,就判断当前元素是否为主元素
- 因为用了快排,时间复杂度O(nlogn),空间复杂度O(logn)
- 总分13分,这个解最高可以拿11分,所以不要强求最优解
int find_main2(SqList list) {
// 1.快排
sort(list.data, list.data+list.length);
// 2.记录每个元素出现的次数,找到主元素
int cnt = 1;
for (int i = 0; i < list.length - 1; i++) {
if (list.data[i+1] == list.data[i]) {
cnt++;
} else {
if (cnt > list.length / 2)
return list.data[i];
cnt = 1;
}
}
return -1;
}
- 这道题考试的时候(应该大概也许)可以不写跟题目无关的排序的具体实现
- 因为快排每次递归需要的空间是固定的,递归层数即为空间复杂度,故快排的平均空间复杂度为O(logn)
- 快排的实现如下:
void quick_sort(int l, int r) {
if (l == r) return;
int x = a[(l+r)/2]; // 1.枢纽元素
// 2.分成两块
int i = l-1, j= r+1;
while (i < j) {
while (a[++i] < x); // 找到左边比x大的元素
while (a[--j] > x); // 找到右边比x小的元素
if (i < j) swap(a[i], a[j]); // 互换
}
// 3.递归
quick_sort(l, j);
quick_sort(j+1, r);
}
- 最优解,对对碰
- 因为主元素个数要超过数组的一半,它就可以抵掉所有其他元素且有剩余
- 时间复杂度O(n),空间复杂度O(1)
int find_main3(SqList list) {
int c, cnt = 1;
c = list.data[0];
// 1.选定候选主元素
for (int i = 1; i < list.length; i++) {
if (list.data[i] == c) cnt++;
else {
if (cnt > 0) {
cnt--;
} else {
c = list.data[i];
cnt = 1;
}
}
}
// 2.统计实际数量
if (cnt > 0) {
cnt = 0;
for (int i = 0; i < list.length; i++)
if (list.data[i] == c)
cnt++;
}
// 3.确认主元素
if (cnt > list.length / 2) return c;
return -1;
}
2.2.3.13
- 达达思路
#include <iostream>
#include <algorithm>
using namespace std;
// 算法思想:首先将数组进行从小到大的排序
// 其次,遍历数组找到第一个正数,若第一个正数不等于1,则返回1。1就是未出现的最小正数
// 若等于1,接着遍历。依次比较相邻两元素是否相差1,直至找到差值不为1,则返回当前数值+1
// 若能遍历到最后一个元素,则直接返回最后一个元素数值+1
int Find_Min(int A[], int n)
{
int i = 0;
while (A[i] <= 0)
{
++i;
}
if (A[i] != 1)
{
return 1;
}
else
{
while (i < n - 1 && A[i + 1] - A[i] == 1)
{
++i;
}
return A[i] + 1;
}
return A[n - 1] + 1;
}
void display(int A[], int n)
{
for (int i = 0; i < n; i++)
{
cout << A[i] << " ";
}
cout << endl;
}
int main()
{
int A[5] = {-5, 1, 3, 2, 3};
sort(A, A + 5);
display(A, 5);
cout << Find_Min(A, 5) << endl;
return 0;
}
-
用了快排 O(nlogn),遍历数组O(n)
所以时间复杂度O(nlogn)+O(n)=O(nlogn) -
空间复杂度O(1)
-
王道算法
-
找最小正整数,所以不需要管负数
-
想法还是跟12题第一种解法一样,类似桶排序
-
开一个新数组,下标对应元素值,值对应元素是否出现
-
时间复杂度O(n),空间复杂度O(n),
竟然已经是最优解了?
int findMissMin(int A[], int n)
{
int i, *B; // 标记数组
// B = int(*) malloc(sizeof(int) * n);
B = new int[n]; // 动态分配空间
memset(B, 0, sizeof(int) * n); // 赋初始值0
for (i = 0; i < n; i++) // 若A[i]的只介于1~n,则标记数组B
{
if (A[i] > 0 && A[i] <= n)
{
B[A[i] - 1] = 1;
}
}
for (i = 0; i < n; ++i) // 扫描数组B,找到目标值
{
if (B[i] == 0)
break;
}
return i + 1; // 返回结果
}
2.2.3.13⭐
- 暴力,直接三重循环!
- 一个一个算,找到最小的
- 时间复杂度O(n3),时间复杂度O(1)
int find_min_distance(int A[], int B[], int C[], int n1, int n2, int n3) {
int dmin = INT_MAX, d;
for (int i = 0; i < n1; i++) {
for (int j = 0; j < n2; j++) {
for (int k = 0; k < n3; k++) {
// 计算距离
d = abs(A[i] - B[j]) + abs(B[j] - C[k]) + abs(C[k] - A[i]);
// 更新最小值
if (d < dmin) dmin = d;
}
}
}
return dmin;
}
-
众所周知,绝对值体现的是数轴上的距离
-
当 a=b=c 时,距离最小
-
当 a<=b<=c 时,如图所示
-
决定D大小的因素就是c和a的距离,所以我们每次固定c找一个使距离最小的a就可以了
-
时间复杂度O(n),空间复杂度O(1)
// n1是否是三个数中的最小值
bool find_min(int n1, int n2, int n3) {
if (n1 <= n2 && n1 <= n3) return true;
return false;
}
int find_min_distance2(int A[], int B[], int C[], int n1, int n2, int n3) {
int dmin = INT_MAX, d, i = 0, j = 0, k = 0;
while(i < n1 && j < n2 && k < n3) {
// 计算距离
d = abs(A[i] - B[j]) + abs(B[j] - C[k]) + abs(C[k] - A[i]);
// 更新最小值
if (d < dmin) dmin = d;
if (find_min(A[i], B[j], C[k])) i++;
else if (find_min(B[j], C[k], A[i])) j++;
else k++;
}
return dmin;
}
二,链表
单链表结构体
#define ElemType int
typedef struct LNode{
ElemType data;
struct LNode* next;
}LNode, *LinkList;
01
- 链表具有天然的递归性,每一个结点都可以看作一个小的链表,它们又可以互相组合成新的链表
- 递归就是函数直接或间接调用函数本身
- 实现递归函数只需要弄明白两件事情:递推关系和递归出口
- 本题中递推关系是:当前结点为x就删除,不为x就处理下一个结点
- 递归出口是:当前结点为空
- 因为递归深度为O(n),所以时间复杂度O(n),空间复杂度O(n)
// 算法思想:首先判断指针指向的当前结点的值是否为x
// 若为x,则进行删除当前结点,并递归判断下一个结点
// 若不为x,则递归判断下一个结点
// 直到指针为空,遍历完整个链表
void Del_x(LinkList &L, ElemType x)
{
LNode *p; // p指向待删除结点
if (L == NULL) // 递归出口
{
return;
}
if (L->data == x) // p所指的结点的值是x,则进行删除
{
p = L; // 则进行删除*L,并让L指向下一个结点
L = L->next;
free(p);
Del_x(L, x); // 递归调用
}
else // p所指的结点的值不是x
{
Del_x(L->next, x); // 递归调用
}
}
02⭐
- 与第一题唯一不同就是带头结点,同样可以使用第一题的递归,函数完全不需要任何改变
void delX(LinkList &L, int x) {
if (L == NULL) return; // 1.递归出口
LNode* p;
if (L->data == x) { // 2.L结点的值为x,删除L结点
p = L;
L = L->next;
free(p);
delX(L, x);
} else { // 3.L结点的值不为x,递归处理L结点的下一个结点
delX(L->next, x);
}
}
// 调用
delX(head->next, 3);
- 经典前后双指针,p从头到尾扫描整个链表,pre指向p的前一个结点便于删除
- 时间复杂度O(n),空间复杂度O(1)
// 经典双指针,p从头到尾扫描整个链表,pre指向p的前一个结点便于删除
void Del_x_1(LinkList &L, ElemType x)
{
LNode *pre = L, *p = L->next, *q; // 置p和pre的初始值
while (p != NULL)
{
if (p->data == x)
{
q = p;
p = p->next;
pre->next = p;
free(q);
}
else
{
pre = p;
p = p->next;
}
}
}
- 实际上条件是任意改变的,只修改if就可以实现,比如删除要求值介于mink和maxk之间所有的结点,则只需要将if语句修改成if(p->data>mink &&p->data<maxk)
// 用尾插法首尾双指针
// 算法思想:逐个扫描结点,当结点值不为x时,尾插至链表尾部。
// 当结点值为时,删除结点。扫描下一个结点,直至链表全部遍历
void Del_x_2(LinkList &L, ElemType x)
{
LNode *r = L, *p = L->next, *q; // r指向尾结点,其初值为头节点
while (p != NULL)
{
if (p->data != x) //*p的值不为x时,将其放在链表尾部
{
r->next = p;
r = p;
p = p->next; // 继续扫描
}
else //*p的值为x时,将其释放
{
q = p;
p = p->next; // 继续扫描
free(q);
}
}
r->next = NULL; // 插入结束后置尾结点指针为NULL
}
03⭐
- 经典递归,出口依旧是空结点
- 利用递归栈反向输出结点值
- 时间复杂度O(n),空间复杂度O(n)
// 2.栈递归实现
void R_Print_1(LinkList L)
{
if (L->next != NULL)//下一还有元素
{
R_Print_1(L->next);//接着递归
}
if (L != NULL)
cout << L->data << " ";//输出结点
}
- 从头到尾遍历整个链表,用栈存储每个结点的值再输出即可
- 时间复杂度O(n),空间复杂度O(n)
void reversePrintList2(LinkList L) {
if (L == NULL) return;
// 1.遍历存储
stack<int> stack;
while (L->next != NULL) {
stack.push(L->next->data);
L = L->next;
}
// 2.输出
while (!stack.empty()) {
cout << stack.top() << " ";
stack.pop();
}
}
04
- 跟顺序表找最小值基本没差,只是需要加一个指针指向前缀便于删除
- 定义双指针:p和前缀pre,同时定义minp和minpre指向当前最小结点
- 时间复杂度O(n),空间复杂度O(1)
// 算法思想:用指针p遍历链表,pre记录遍历指针的前驱结点。
// minp记录最小值的结点,minpre记录最小值的前驱结点
// 当遍历指针p的值比最小值结点的值,就更新。
// 否则继续遍历
void Del_Min(LinkList &L)
{
// 初始默认第一个结点为最小值结点
LNode *pre = L, *p = L->next; // p为工作指针,pre指向其前驱
LNode *minpre = pre, *minp = p; // minp最小值结点,minpre最小值的前驱结点
while (p != NULL)
{
if (p->data < minp->data) // 找到比之前更小的结点,就更新
{
minp = p;
minpre = pre;
}
else
{
pre = p;
p = p->next;
}
}
minpre->next = minp->next; // 删除结点
free(minp);
}
05
- 就地逆置,对于带头结点的链表来说就是把头结点之后的结点按照头插法再插入
- 还是双指针,这次需要p以及p的后缀r (画图更容易理解) r的作用是防止链断裂
- 头插也就是说每次都把新的p插入到L后面!这样L依旧是头结点,不需要返回新的链表
- 时间复杂度O(n),空间复杂度O(1)
// 1.用头插法将链表逆置
void Reverse(LinkList &L)
{
LNode *p, *r; // p为工作指针,r为后继指针,以防止断链
p = L->next; // 从第一个元素结点开始
L->next = NULL; // 先将头节点L的next域置为NULL
while (p != NULL) // 依次将元素结点摘下来
{
r = p->next; // 缓存p的后继
p->next = L->next; // 将p结点插到头节点之后
L->next = p;
p = r;
}
}
- 结点的相对物理位置不变,指针方向改变实现逆转
- 时间复杂度O(n),空间复杂度O(1)
// 2.结点的相对物理位置不变,指针方向改变实现逆转
void Reverse_1(LinkList &L)
{
// p是遍历结点,pre是其前驱结点,r是其后继结点
LNode *pre, *p = L->next, *r = p->next; // pre的作用是找到逆转的方向结点,r是防止链表断裂
p->next = NULL; // 处理第一个结点,将其与后面接点断开
while (r != NULL) // r为空则说明p是最后一个结点
{
pre = p; // 依次遍历
p = r;
r = r->next;
p->next = pre; // 指针反转
}
L->next = p; // 处理最后一个结点,使头节点的下一个结点指向原来的尾结点,实现链表的反转
}
06⭐
- 直接插入排序,首先构造一个只有一个结点的有序列表pre
- 剩下的p链表中的元素分别找位置插入即可
- 时间复杂度O(n2),空间复杂度O(1)
直接插入排序的链表实现
void Sort(LinkList &L)
{
// 默认第一个结点值是最小的,选择插入排序从第二个元素开始
LNode *p = L->next, *pre; // p初始指向第一个结点,pre指向被插入位置的前驱结点
LNode *r = p->next; // r指向p的后继结点,防止链表断裂
p->next = NULL; // 将第一个结点与后面的结点断开
p = r;
while (p != NULL)
{
r = p->next; // 保存指针p的后继结点指针
pre = L; // 从头开始查找合适的插入位置
while (pre->next != NULL && pre->next->data < p->data)
{
pre = pre->next; // 有序表中查找插入*p的前驱结点*pre
}
p->next = pre->next;
pre->next = p;
p = r; // 扫描原来链表中剩下的结点
}
}
直接插入排序顺序存储实现
void InsertSort(int A[], int n)
{
int i, j;
int temp; // 用暂存待排序数
for (i = 1; i < n; ++i)
{
if (A[i] < A[i - 1])
{
temp = A[i];
for (j = i - 1; j >= 0 && temp < A[j]; --j)
{
A[j + 1] = A[j]; // 将大于待排序数temp的元素往后移一位
}
A[j + 1] = temp; // 复制到插入的位置
}
}
}
- 把链表数据取出来放到数组里排序,然后再将排好序的数放回链表
- 典型的空间换时间,排序时间复杂度O(nlog2n),空间复杂度O(n)
void sortList(LinkList &L, int len) {
// 1.将链表数据复制到数组中
LNode *head = L->next;
int a[len], i = 0;
while (head != NULL) {
a[i++] = head->data;
head = head->next;
}
// 2.排序
sort(a, a+len);
// 3.将排序后的数组复制回链表
head = L->next, i = 0;
while (head != NULL) {
head->data = a[i++];
head = head->next;
}
}
08⭐
- 首先要理解公共结点的含义,当两个链表从某一个结点开始,指针都指向同一个结点,那么这个结点就是公共结点,整个形状呈现一个Y型
- - 暴力解法,双重循环,遍历第一个链表的结点的同时遍历第二个链表所有的结点,找到相同点
- 时间复杂度O(n^2),或者说O(len1×len2),空间复杂度O(1)
LNode* findCommon(LinkList A, LinkList B) {
LNode *pa = A->next, *pb = B->next;
while (pa != NULL) {
while (pb != NULL) {
if (pa == pb)
return pa;
pb = pb->next;
}
pa = pa->next;
pb = B->next; // 重置指针
}
return NULL; // 没找到返回NULL
}
- 如果两个链表有公共结点,那么在公共结点后的所有结点都是重合的,我们只需要将链表的长度整成相同的,然后向后找第一个相同点即可
- 先分别获得两个链表的长度,做差,将其长度置为相同
- 同步遍历,找到相同结点即可
- 时间复杂度O(n),空间复杂度O(1)
int getLen(LinkList L)
{
int len = 0;
while (L->next != NULL)
{
L = L->next;
len++;
}
return len;
}
LNode *findCommon(LinkList L1, LinkList L2)
{
// 1.计算A, B的长度
int lenA = getLen(L1), lenB = getLen(L2);
// 2.让A, B的长度差距为0
if (lenA > lenB)
{
for (int i = 0; i < lenA - lenB; i++)
L1 = L1->next;
}
else
{
for (int i = 0; i < lenB - lenA; i++)
L2 = L2->next;
}
// 3.开始比较
while (L1 != NULL)
{
if (L1 == L2)
return L1;
L1 = L1->next;
L2 = L2->next;
}
// 4.没找到返回NULL
return NULL;
}
09
算法思想1:与第六题的思想一样,可以使用直接插入排序的链表实现,使链表递增有序。
然后依次遍历输出并释放结点。
时间复杂度O(n2),空间复杂度O(1)
算法思想2:
- 按照递增次序输出结点,然后删除
- 其实就是每次找到链表的最小值元素,输出后删除
- 删除一个元素需要记录它的前驱结点
- 时间复杂度O(n2),空间复杂度O(1)
- 如果可以用数组,就把元素拿出来放到数组里排序,时间复杂度O(nlog2n),空间复杂度O(n)
void delMin(LinkList &L) {
// 1.只剩一个头结点时停止
while (L->next != NULL) {
LNode *minpre = L, *p = L->next;
// 2.找到最小值,记录其前缀
while (p->next != NULL) {
if (p->next->data < minpre->next->data)
minpre = p;
p = p->next;
}
// 3.输出并释放最小值结点
cout << minpre->next->data << ' ';
LNode *del = minpre->next;
minpre->next = del->next;
free(del);
}
// 4.释放头结点
free(L);
}
10
- 遍历,设一个变量查看奇偶,分别插入
- 保持相对顺序不变,采用尾插法
- 时间复杂度O(n),空间复杂度O(1)
LinkList DisCreat(LinkList &L)
{
int i = 0; // i记录表中结点的序号
LinkList L1 = new LNode; // 创建L1表头,指向偶数序列
L1->next = NULL; // L1表的初始化
LNode *Lr = L, *L1r = L1; // Lr 和L1r分别指向各自表的尾结点
LNode *p = L->next; // p为链表的遍历工作指针,指向待分解的结点
L->next = NULL; // 置空新的A表
while (p != NULL)
{
++i; // 序列加1
if (i % 2 != 0) // 处理奇数序号结点
{
Lr->next = p;
Lr = p;
}
else // 处理偶数序号结点
{
L1r->next = p;
L1r = p;
}
p = p->next; // 遍历下一个结点
}
Lr->next = NULL; // 末结点下一个结点置空
L1r->next = NULL;
return L1;
}
void splitList2(LinkList L, LinkList &A, LinkList &B) {
// 1.创建工作指针
LNode *p = L->next, *pa = A, *pb = B;
while (p != NULL) {
// 2.采用尾插法分别插入
pa->next = p;
pa = p;
p = p->next; // 继续处理
if (p != NULL) {
pb->next = p;
pb = p;
p = p->next; // 继续处理
}
}
// 3.最后设为空
pa->next = NULL;
pb->next = NULL;
}
11
- 与上一题类似,不过在插入B时需要采用头插法使其逆序
- 头插时要注意记录后继元素,防止断链
- 就地算法,时间复杂度O(n),空间复杂度O(1)
void splitList(LinkList L, LinkList &A, LinkList &B) {
// 1.创建工作指针
LNode *p = L->next, *pa = A;
int i = 1;
while (p != NULL) {
// 2.分别采用头插法和尾插法插入
if (i % 2 != 0) {
pa->next = p;
pa = p;
p = p->next;
} else {
LNode *q = p->next; // 记录p的后继防止断链
p->next = B->next;
B->next = p;
p = q;
}
i++;
}
// 3.收尾
pa->next = NULL;
}
- 同上也可以不使用变量记录奇偶
void splitList2(LinkList L, LinkList &A, LinkList &B) {
// 1.创建工作指针
LNode *p = L->next, *pa = A;
while (p != NULL) {
// 2.分别采用头插法和尾插法插入
pa->next = p;
pa = p;
p = p->next;
if (p != NULL) {
LNode *q = p->next; // 记录p的后继防止断链
p->next = B->next;
B->next = p;
p = q;
}
}
// 3.收尾
pa->next = NULL;
}
12
- 有序表,直接循环扫描
- 判断是否相等,相等直接删除,释放空间
- 不等就下一个
- 时间复杂度O(n),空间复杂度O(1
void Del_same(LinkList &L)
{
LNode *p = L->next, *q; // p为扫描工作指针,p初始值为第一个结点
if (L == NULL)
{
return;
}
while (p->next != NULL)
{
q = p->next; // q指向q的后继结点
if (p->data == q->data)//找到重复的结点
{
p->next = q->next;
free(q);//释放相同结点
}
else
{
p = p->next;//遍历下一个结点
}
}
}
13
- 经典合并链表!不过是两个递增合并为一个递减,头插即可
- 遍历完后,肯定有一个链表有剩余元素,将未遍历完的链表依次头插至L
- 时间复杂度O(n),空间复杂度O(1)
LinkList MergeList(LinkList &L1, LinkList &L2)
{
LNode *p = L1->next, *q = L2->next, *pr, *qr; // p,q 分别是L1 L2的工作指针
LinkList L = L1; // 新设立一个指针L,初始指向L1,用于合并两链表
L->next = NULL;
while (p != NULL && q != NULL) // 遍历L1与L2
{
pr = p->next; // 指向L1工作指针的下一个结点,防止断链
qr = q->next; // 指向L2工作指针的下一个结点,防止断链
if (p->data <= q->data)
{
p->next = L->next;
L->next = p;
p = pr;
}
else
{
q->next = L->next;
L->next = q;
q = qr;
}
}
while (p != NULL)//若L1链没有遍历完,依次头插入L
{
pr = p->next;
p->next = L->next;
L->next = p;
p = pr;
}
while (q != NULL)//若L2链没有遍历完,依次头插入L
{
qr = q->next;
q->next = L->next;
L->next = q;
q = qr;
}
return L;
}
14
- 算法思想:依次比较A B 两链表的元素,若值不相等,小的元素后移。若值相等,创建一个新节点
- 节点值等于 两节点的值,使用尾插法插入新的链表中,并将两个原指针都后移一位,直至其中一个链表遍历到表尾
LinkList Get_Common(LinkList A, LinkList B)
{
LNode *p = A->next, *q = B->next, *r, *s;
LinkList C = new LNode; // 建立表C
r = C; // r始终指向C的尾结点
while (p != NULL && q != NULL)
{
if (p->data < q->data)
{
p = p->next; // 若A的当前元素较小,后移指针B
}
else if (p->data > q->data)
{
q = q->next; // 若B的当前元素较小,后移指针
}
else // 找到了公共元素结点
{
s = new LNode;
s->data = p->data; // 复制产生结点*s
r->next = s; // 将*s复制到C上(尾插法)
r = s;
p = p->next; // p q 都后移 ,继续扫描。
q = q->next;
}
}
r->next = NULL; // 置C尾结点指针为空
return C;
}
15
- 跟上一题差不多,只是不让存入新链表,思路还是一样的
- 我们只需要把A链表当作一个新链表,记录A链表的尾结点,然后每次尾插就可以了
- 这就是归并的思想,同步遍历两个链表
- 时间复杂度O(len1+len2),空间复杂度O(1)
LinkList Union(LinkList &La, LinkList &Lb)
{
LNode *pa = La->next; // 设立工作指针分别为pa,pb
LNode *pb = Lb->next;
LNode *u, *pc = La; // pc指向结果表中的尾结点
while (pa && pb)
{
if (pa->data == pb->data) // 交集并入结果表中,通过pc尾插法
{
pc->next = pa; // A中的结点接入表中
pc = pa;
pa = pa->next;
u = pb; // B中的结点释放
pb = pb->next;
free(u);
}
// 较小的结点后移,并释放当前结点
else if (pa->data < pb->data)
{
u = pa;
pa = pa->next;
free(u);
}
else
{
u = pb;
pb = pb->next;
free(u);
}
} // while结束
// 将未遍历完的剩余结点,全部释放
while (pa)
{
u = pa;
pa = pa->next;
free(u);
}
while (pb)
{
u = pb;
pb = pb->next;
free(u);
}
pc->next = NULL; // 置结果链表尾指针为NULL
free(Lb); // 释放B表的头结点
return La;
}
16⭐
- 判断连续子序列的链表版,其实跟字符串是一样的
- 两个链表都从第一个结点开始比较,如果不同
则A链表从下一个结点开始,B链表从头开始 - B链表能匹配到表尾表示匹配成功
- 时间复杂度O(n2),空间复杂度O(1)
bool Pattern(LinkList A, LinkList B)
{
LNode *p = A->next; // p为A链表的工作指针
LNode *pre = p; // pre记住每次比较的开始结点
LNode *q = B->next; // q为B链表的工作指针
while (p && q)
{
if (p->data == q->data) // 值相同
{
p = p->next;
q = q->next;
}
else
{
pre = pre->next;
p = pre; // A链表新的开始比较结点
q = B->next; // q从B链表的第一个结点开始
}
}
if (!q) // B已经比较结束
{
return true; // 说明B是A的子序列
}
else
{
return false;
}
}
- 可以进一步优化KMP代码实现
17
- 头尾对比,只要有一个不同就不对称
- 注意循环条件
- 时间复杂度O(n),空间复杂度O(1)
bool Symmetry(DLinkList L)
{
DNode *p = L->next, *q = L->prior; // 两头工作指针
while (p != q && q->next != p) // 循环跳出条件
{
if (p->data == q->data)
{
p = p->next;
q = q->prior;
}
else
{
return false;
}
}
return true;
}
18
// 创建一个带头结点的循环单链表
LinkList createHeadList(vector<int> data) {
if (data.size() == 0) return NULL;
LNode* head = (LinkList)malloc(sizeof(LNode));
head->next = NULL;
LNode* p = head;
for (int i = 0; i < data.size(); i++) {
LNode* q = (LNode*)malloc(sizeof(LNode));
q->data = data[i];
q->next = NULL;
p->next = q;
p = q;
}
p->next = head; // 最后指向头指针
return head;
}
void printList(LinkList L) {
LNode *head = L->next;
while (head != L) {
cout << head->data << " ";
head = head->next;
}
puts("");
}
- 循环单链表结构体及创建函数(可跳过)
- 首先找到h1的尾结点
- 然后找到h2的尾结点指向h1头结点
- 时间复杂度O(len1+len2),也就是O(n),空间复杂度O(1)
void linkTwoLists(LinkList h1, LinkList h2) {
// 1.创建工作指针
LNode *p1 = h1->next, *p2 = h2->next;
// 2.找到h1的尾结点
while (p1->next != h1) {
p1 = p1->next;
}
// 3.链接h2
p1->next = p2;
// 4.找到h2的尾结点,指向h1
while(p2->next != h2) {
p2 = p2->next;
}
p2->next = h1;
}
19
- 遍历循环单链表,每循环一次查找一个最小结点(循环单链表,注意循环终止条件!)
- minp指向最小值结点,minpre指向其前驱
- 输出其值然后删除,循环结束后释放头结点
- 时间复杂度O(n2),空间复杂度O(1)
void delMin(LinkList L) {
// 1.创建工作指针,遍历
LNode *p, *pre, *minp, *minpre;
while (L->next != L) {
pre = L, p = L->next;
minpre = pre, minp = p;
// 2.找到最小值,记录其前缀
while (p != L) {
if (p->data < minp->data) {
minp = p;
minpre = pre;
}
pre = p;
p = p->next;
}
// 3.输出并释放最小值结点
cout << minp->data << ' ';
minpre->next = minp->next;
free(minp);
}
// 4.释放头结点
free(L);
}
- 每次删除最小值结点,我们只需要记录最小值前缀即可
- 每次都用 minpre->next 进行比较
- 时间复杂度O(n2),空间复杂度O(1)
void delMin2(LinkList L) {
// 1.只剩一个头结点时停止
while (L->next != L) {
LNode *minpre = L, *p = L->next;
// 2.找到最小值,记录其前缀
while (p->next != L) {
if (p->next->data < minpre->next->data)
minpre = p;
p = p->next;
}
// 3.输出并释放最小值结点
cout << minpre->next->data << ' ';
LNode *del = minpre->next;
minpre->next = del->next;
free(del);
}
// 4.释放头结点
free(L);
}
- 如果只考虑输出的话,有一个取巧的方法
- 我们把链表的值取出来放在一个数组中排序输出
- 最后释放链表空间即可
- 用了cpp的 sort() ,所以时间复杂度O(nlog2n),空间复杂度O(n)
int getLength(LinkList L) {
int i = 0;
LNode *p = L->next;
while (p != L) {
p = p->next;
i++;
}
return i;
}
void delMin3(LinkList L) {
int len = getLength(L);
int a[len], i = 0;
LNode *head = L->next;
while (head != L) {
a[i++] = head->data;
head = head->next;
}
sort(a, a+len);
for (int i = 0; i < len; i++)
cout << a[i] << ' ';
puts("");
// 释放所有结点空间,此处省略
}
20
- 找到值为x的结点,取下来
- 如果该结点是第一个元素或者freq小于前驱结点,直接返回即可
- 顺着该结点的前驱向前找到第一个freq大于自己的结点,插到它后面
- 最后返回该结点的地址,即指针
- 双链表插入删除一定要注意空结点位置
- 时间复杂度O(n),空间复杂度O(1)
// 按照访频从大往小的选着插入排序
DNode *locate(DLinkList &L, ElemType x)
{
DNode *p = L->next, *q; // p为工作指针,q为p的前驱,用于查找插入的位置
while (p && p->data != x)
{
p = p->next; // 查找值为x的结点
}
if (!p)
{
exit(0); // 不存在值为x的结点
}
else
{
p->freq++; // 令元素值为x的结点的freq域加1
if (p->pre == L || p->pre->freq > p->freq) // p是链表首结点,或preq的值小于前驱,这两种情况都不需要进行遍历前插
{
return p; // 直接返回地址
}
// 将p结点从链表中摘取下来。,并保持不断链
if (p->next != NULL)
{
p->next->pre = p->pre;
}
p->pre->next = p->next;
// 以下是查找p的结点的插入位置的前一个结点
q = p->pre;
while (q->freq <= p->freq && q != L)
{
q = q->pre;
}
// 将p结点排在同频率的第一个
p->next = q->next;
if (q->next != NULL)
{
q->next->pre = p;
}
p->pre = q;
q->next = p;
}
return p; // 返回值为x的结点指针
}
- 完整函数代码
#include <iostream>
using namespace std;
#define ElemType int
typedef struct DNode
{
ElemType data;
struct DNode *next;
struct DNode *pre;
int freq;
} DNode, *DLinkList;
// 带头结点的双向链表
bool InitList(DLinkList &L)
{
L = new DNode;
if (L == NULL) // 开辟空间失败
{
return false;
}
L->next = NULL;
L->pre = NULL;
return true;
}
// 用尾插法建立有头节点的双向链表
bool List_TailInsert(DLinkList &L) // 正向建立链表
{
int x; // 临时变量
DNode *s, *r; // s 表示临时指针,r表示尾指针,指向链表的尾部
r = L;
cin >> x;
while (x != 9999)
{
s = new DNode;
s->data = x;
s->freq = 0;
r->next = s;
s->pre = r;
r = s;
cin >> x;
}
r->next = NULL;
return true;
}
// 打印输出链表
void showList(DLinkList L)
{
cout << "双链表为:";
DNode *p = L;
while (p->next != NULL)
{
p = p->next;
cout << p->data << " ";
}
cout << endl;
}
// 按照访频从大往小的选着插入排序
DNode *locate(DLinkList &L, ElemType x)
{
DNode *p = L->next, *q; // p为工作指针,q为p的前驱,用于查找插入的位置
while (p && p->data != x)
{
p = p->next; // 查找值为x的结点
}
if (!p)
{
exit(0); // 不存在值为x的结点
}
else
{
p->freq++; // 令元素值为x的结点的freq域加1
if (p->pre == L || p->pre->freq > p->freq) // p是链表首结点,或preq的值小于前驱,这两种情况都不需要进行遍历前插
{
return p; // 直接返回地址
}
// 将p结点从链表中摘取下来。,并保持不断链
if (p->next != NULL)
{
p->next->pre = p->pre;
}
p->pre->next = p->next;
// 以下是查找p的结点的插入位置的前一个结点
q = p->pre;
while (q->freq <= p->freq && q != L)
{
q = q->pre;
}
// 将p结点排在同频率的第一个
p->next = q->next;
if (q->next != NULL)
{
q->next->pre = p;
}
p->pre = q;
q->next = p;
}
return p; // 返回值为x的结点指针
}
int main()
{
DLinkList L;
InitList(L);
List_TailInsert(L);
showList(L);
cout << locate(L, 2)->data << endl;
showList(L);
return 0;
}
21⭐
-
这个题是leetcode上很经典的一道链表题
-
看到环我们就要想到快慢指针
-
两个指针,一个fast每次走两步,一个slow每次走一步
-
如果有环的话,fast一定会先进入环,slow后进入环,两个指针都进入环后继续循环一定会相遇
-
同理,相遇则证明环存在,此题已经解决
-
时间复杂度O(n),空间复杂度O(1)
-
当然你也可以采用暴力做法,遍历然后记录每个出现的结点,如果结点再次出现,则证明有环。这样的时间复杂度为O(n),空间复杂度也是O(n)
-
但是通常要求不会那么简单,一定是让你返回环的入口点
-
涉及到入口点,我们就要来算一道数学题
-
假设环内一共有r个元素,链表头结点到环入口点有a个结点,环入口点到相遇点的距离为x(顺时针),快指针已经绕了n圈
-
则有 a+nr+x = 2(a+x),解得 a = nr-x
-
因此设置一个指针指向head,一个指向相遇点,同步移动,相遇点即为入口点
-
时间复杂度O(n),空间复杂度O(1)
LNode *FindLoopStart(LNode *head)
{
LNode *fast = head, *slow = head; // 设置快慢两个指针
while (fast != NULL && fast->next != NULL) // 链表非空,进行寻找环节的操作
{
slow = slow->next;
fast = fast->next->next;
if (slow == fast) // 当快慢指针相遇,跳出循环
break;
}
if (fast == NULL || fast->next == NULL) // 没有环,返回NULL
{
return NULL;
}
LNode *p1 = head, *p2 = slow; // 分别指向开始结点和相遇结点
while (p1 != p2)
{
p1 = p1->next;
p2 = p2->next;
}
return p1;
}
22
- 首先想到暴力方法,先get到链表的长度n
- 然后遍历找到第n-k个结点
- 时间复杂度O(n),空间复杂度O(1)
int searchK(LinkList L, int k) {
// 1.获取链表长度n
int n = 0;
LNode *p = L->link;
while (p != NULL) {
p = p->link;
n++;
}
// 2.没有那么多结点,直接返回0
if (n < k) return 0;
// 3.遍历找到第n-k个结点
int count = n - k;
p = L->link;
while (count--) {
p = p->link;
}
cout << "倒数第" << k << "个结点值为" << p->data << endl;
return 1;
}
- 最优肯定要一次遍历解决,自然而然想到双指针也是快慢指针
- 既然要找到第n-k个结点,我们可以先让前一个指针走k步
- 然后两个指针同步走,等到前一个指针走到结尾,后一个指针自然就走到了n-k结点处
- 时间复杂度O(n),空间复杂度O(1)
int Search_k(LinkList L, int k)
{
LNode *p = L->next, *q = L->next; // 指针p ,q指示第一个结点
int count = 0;
while (p != NULL) // 遍历链表直到最后一个结点
{
if (count < k) // 计数,若count<K 只移动p
count++;
else
q = q->next;
p = p->next; // 之后,让p,q同步移动
}
if (count < k)
{
return 0; // 查找失败返回0
}
else // 查找成功打印并返回1
{
printf("%d", q->data);
}
return 1;
}
-
当然也有一些其他的奇淫巧计
-
在我们学了栈之后,利用它的“后进先出”的特性,先把所有值压入栈中,再弹出k个元素,最后一个出栈元素即所求值
-
如果你对递归足够了解,你也可以尝试使用递归解决这个问题
-
终止条件就是遍历结点为空,触底反弹,往回走的过程中记录走过的结点,达到k就是倒数第k个结点,直接返回即可
-
如果没有达到k,直接返回NULL即可
-
递归算法的实现
// 递归实现
int count = 0;
int Search_k1(LinkList L, int k, LNode *r)
{
LNode *p = L->next;
while (p->next != r)
{
p = p->next;
}
if (k == count)
{
cout << p->next->data;
return 1;
}
else
{
count++;
return Search_k1(L, k, p);
}
}
23⭐
- 跟第8题基本一样,经典双指针
- 我们可以先让表尾对齐, 比如 loading 和 being 要让str1指向a,str2指向b
- 然后同步遍历即可,两个指针指向同一个结点即共同后缀
- 时间复杂度O(max(len1, len2)),也算O(n)吧,空间复杂度O(1)
int getLen(LinkList L) {
int len = 0;
while (L->next != NULL) {
L = L->next;
len++;
}
return len;
}
// A即str1, B即str2
LNode* findCommon(LinkList A, LinkList B) {
// 1.计算A, B的长度
int lenA = getLen(A), lenB = getLen(B);
// 2.让A, B的长度差距为0
if (lenA > lenB) {
for (int i = 0; i < lenA - lenB; i++)
A = A->next;
} else {
for (int i = 0; i < lenB - lenA; i++)
B = B->next;
}
// 3.开始比较
while (A != NULL) {
if (A == B)
return A;
A = A->next;
B = B->next;
}
// 4.没找到返回NULL
return NULL;
}
- 当然它是有暴力做法的,你可以使用双重循环,遍历第一个链表的结点的同时遍历第二个链表所有的结点,找到相同点
- 时间复杂度O(len1×len2),空间复杂度O(1)
LNode* findCommon2(LinkList A, LinkList B) {
LNode *pa = A->next, *pb = B->next;
while (pa != NULL) {
while (pb != NULL) {
if (pa == pb)
return pa;
pb = pb->next;
}
pa = pa->next;
pb = B->next; // 重置指针
}
return NULL; // 没找到返回NULL
}
- 这道题想要运行,我们需要稍微改变一下结构体形式以及其他函数(可跳过)
#include <bits/stdc++.h>
#define ElemType char
using namespace std;
typedef struct LNode{
ElemType data;
struct LNode* next;
}LNode, *LinkList;
LinkList createHeadList(vector<ElemType> data) {
if (data.size() == 0) return NULL;
LNode* head = (LinkList)malloc(sizeof(LNode));
head->next = NULL;
LNode* p = head;
for (int i = 0; i < data.size(); i++) {
LNode* q = (LNode*)malloc(sizeof(LNode));
q->data = data[i];
q->next = NULL;
p->next = q;
p = q;
}
return head;
}
void printList(LinkList L) {
while (L != NULL) {
cout << L->data;
L = L->next;
}
puts("");
}
int main() {
vector<char> dataA{'l', 'o', 'a', 'd'};
vector<char> dataB{'b', 'e'};
vector<char> dataC{'i', 'n', 'g'};
LinkList headA = createHeadList(dataA);
LinkList headB = createHeadList(dataB);
LinkList headC = createHeadList(dataC);
LNode *p1 = headA->next, *p2 = headB->next, *p3 = headC->next;
while (p1->next != NULL) p1 = p1->next;
while (p2->next != NULL) p2 = p2->next;
p1->next = p3;
p2->next = p3;
cout << "链表A: ";
printList(headA->next);
cout << "链表B: ";
printList(headB->next);
return 0;
}
24
- 首先给出单链表结点的数据类型定义:
typedef struct LNode{
int data;
struct LNode* link;
}LNode, *LinkList;
- 一眼桶排,之前做过很多次这样的题,其实- 就是用一个辅助数组记录出现过的结点值
其实就是把出现的结点值看作数组下标,如果需要记录次数则数组元素代表出现次数 - 第一个数为21,则令辅助数组 arr[21]=1 即可
- 因为 |data| <= n,只需要保存绝对值,所以申请一个大小为 n+1 的辅助数组就可以了
- 我们可以一边遍历,一边判断结点元素值的绝对值是否是第一次出现,不是则删除该结点
- 典型的拿空间换时间,时间复杂度O(m),空间复杂度O(n)
void delSameAbsoluteValue(LinkList L, int n) {
// 1.初始化辅助数组
int arr[n+1] = {0};
LNode *p = L, *del;
// 2.遍历同时判别
while (p->link != NULL) {
int m = abs(p->link->data); // 获得绝对值
if (arr[m] == 0) {
arr[m] = 1; // 首次出现
p = p->link;
} else {
del = p->link; // 非首次出现直接删除
p->link = del->link;
free(del);
}
}
}
- 有一点需要说明一下
- 408考试的时候,好像是不允许 int arr[n+1] = {0} 这种语法的
- 所以我们需要用 malloc() 来创建数组,然后使用循环或者 memset() 将数组元素初始值置为0
- 注意 memset() 只能将int数组初始化0或-1
int arr[n+1] = {0};
// 推荐写法
int *arr = (int *)malloc(sizeof(int) * (n+1));
for (int i = 0; i < n+1; i++) {
*(arr+i) = 0;
}
int *arr = (int *)malloc(sizeof(int) * (n+1));
memset(arr, 0, sizeof(int) * (n+1));
int arr[n+1];
memset(arr, 0, sizeof(arr));
25
- 要求时间复杂度O(1),所以很多取巧暴力的方法就不能用了
- 我们需要把链表均分为两份,然后重新排列,所以需要先找到中间结点
- 链表找中间节点有一种固定使用的方法,就是快慢指针
- 设置两个指针p和q,p每次走一步,q每次走两步
- 当指针q到达链表尾时,p就正好在链表中间(可以自己画一个图,非常好理解)
- 从中间结点断开,分成两个链表
- 然后将后一个链表也就是原链表的后半段进行原地逆置
- 最后分别插入即可
- 时间复杂度O(n),空间复杂度O(1)
void rearrange(NODE *head) {
NODE *p = head, *q = head, *tmp, *s;
// 1.快慢指针,找到中间结点
while(q->next != NULL) {
p = p->next;
q = q->next;
if(q->next != NULL) q = q->next;
}
// 2.断链,分成两个链表
q = p->next;
p->next = NULL;
// 3.后一个链表原地逆置,链回去
while (q != NULL) {
tmp = q->next;
q->next = p->next;
p->next = q;
q = tmp;
}
// 4.拼接两个链表
s = head->next;
q = p->next;
p->next = NULL;
while (q != NULL) {
tmp = q->next;
q->next = s->next;
s->next = q;
s = q->next;
q = tmp;
}
}
- 思路就是要插入逆置的后半部分的链表,而逆置后半部分就需要先找到中间结点,找中间结点就要用到快慢指针