顺序表和链表
【目录】
1.线性表
2.顺序表
3.链表
4.顺序表和链表的区别和联系
5.顺序表和链表的相关选择题
1.线性表
线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串…
延申:什么是数组? 相同类型元素的一个有限集合,里面放的元素和数据,和线性表是一样的
数组和线性表的其中一个结构类似
线性表相当于在数组的基础之上绑定了一些 增删查改 等其他操作
数组只是一种存储数据的一种方式,没有其他的操作
常见的数组有:顺序表,链表,字符串
字符串:我们C语言中的给的“hello”这种字符串,这种字符串就存储在线性空间里面
栈,队列,我们可以把他看作成为一种特殊的线性表
线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物
理上存储时,通常以数组和链式结构的形式存储。
解释:
线性表在逻辑上是线性结构,也就说是连续的一条直线:
(或者线段,线段更为准确)
(因为里面存储的数据必定是有限的,我们不可能无限的存储,因为我们的内存是有限的)
逻辑上:我们想象中的,实际上他在内存中的存储不一定
举例:先找一个数组,在里面放入【1,2,3,4,5】(假如我们在里面存储了五个元素)
这五个元素在里面存储的时候是连续的空间中
这种结构我们可以暂时把他称之为:顺序表
为什么我们把它称之为线性结构
它里面每一个元素都有唯一的前驱和后继(例如:数字3,有唯一的前驱2,唯一的后继4)
当然有两个元素比较特殊,1和5,
1只有后继,5只有前驱
我们把这种数据结构,我们再往上面绑定上一些,增删查改等操作,
我们就把这种结构称之为顺序表
这种结构有自己的优点:
因为元素存储再连续的空间里面,我们访问里面的任意元素就会比较高效
例如:我们要访问里面的元素3,我们只需要输入arr2就可以访问并拿到这个元素
这种结构也有自己的缺点:
例如:我们要在二和三中间插入一个元素0
我们不可能直接往里面插入,直接插入就可能将三直接覆盖掉
我们需要将三以后包括三的所有元素往后面搬移一个位置,
我们才可以将0插入进去
最差情况下我们需要将0插入最开始的位置,此时我们需要将所有元素后移一位
在任意位置里面插入元素的效率特别低
因此插入一个元素的时间复杂夫为O(N)
有时候我们需要再数组里面插入的元素比较多,
此时,我们每次插入一个元素都要将其后面的所有元素往后移一位(效率特别低)
顺序表里面有顺序的,也有链式的
我们不想要搬移元素,我们就不需要让这些元素紧挨在一起
例如:链表
【1】 【2】 【3】 【4】
将其分开存储,此时我们便有疑问?我们怎么知道【1】后面的元素是谁?
我们再【1】里面保存【2】的地址
例如:
0x1234 0x2345 0x3456 0x4567
【1】 【2】 【3】 【4】
0x2345 0x3456 0x4567 NULL
这种线性表的优点:
例如:我们要在【2】和【3】中间插入一个元素【0】(【0】地址:0x5678)
1.我们先找一个空间将【0】保存起来
0x1234 0x2345 0x5678 0x3456 0x4567
【1】 【2】 【0】 【3】 【4】
0x2345 0x5678 0x3456 0x4567 NULL
2.顺序表
2.1概念及结构
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。
顺序表一般可以分为:
- 静态顺序表:使用定长数组存储。
- 动态顺序表:使用动态开辟的数组存储。
顺序表的静态存储
#define N 100 //宏:定义N为100
typedef int SLDataType;
typedef struct SeqList //SeqList:顺序表
{
SLDataType array[N]; // 定长数组
size_t size; // 有效数据的个数
}SeqList;
解释:
SLDataType array[N]; // 定长数组
我们定义一个数组的大小
例如:
【1 2 3 4 5 】
我们定义了数组的空间大小,虽然里面只放了5个元素但是里面的空间还是100(给了空间没有占满)
size_t size; // 有效数据的个数
我们定义了数组的空间大小,但是我们不知道里面有几个元素
【size】将里面的元素记录起来
缺点:我们只能放100个元素,里面的空间大小确定了
改良版:静态只能存储固定的,我们需要动态的(可以改变存储大小的顺序表)
顺序表的动态存储
typedef struct SeqList
{
SLDataType* array; // 指向动态开辟的数组(起始位置)
size_t size; // 有效数据个数
size_t capicity; // 容量空间的大小(临时开辟的空间大小,当需要增容时,这个数值会改变)
}SeqList;
2.2 接口实现:
静态顺序表只适用于确定知道需要存多少数据的场景。静态顺序表的定长数组导致N定大了,空间开多
了浪费,开少了不够用。所以现实中基本都是使用动态顺序表,根据需要动态的分配空间大小,所以下
面我们实现动态顺序表。
https://blog.csdn.net/sakeww/article/details/106919875
2.3 数组相关面试题
以下都是一些 oj类型考试(线上类型考试,在线考试)
- 原地移除数组中所有的元素val,要求时间复杂度为O(N),空间复杂度为O(1)。
移除元素:
给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。
不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并 原地 修改输入数组。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
示例 1:
给定 nums = [3, 2, 2, 3], val = 3,
函数应该返回新的长度 2, 并且 nums 中的前两个元素均为 2。
你不需要考虑数组中超出新长度后面的元素。
示例 2 :
给定 nums = [0, 1, 2, 2, 3, 0, 4, 2], val = 2,
函数应该返回新的长度 5, 并且 nums 中的前五个元素为 0, 1, 3, 0, 4。
注意这五个元素可为任意顺序。
你不需要考虑数组中超出新长度后面的元素。
说明 :
为什么返回数值是整数,但输出的答案是数组呢 ?
请注意,输入数组是以「引用」方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。
你可以想象内部操作如下 :
nums 是以“引用”方式传递的。也就是说,不对实参作任何拷贝
int len = removeElement(nums, val);
在函数里修改输入数组对于调用者是可见的。
根据你的函数返回的长度, 它会打印出数组中 该长度范围内 的所有元素。
for (int i = 0; i < len; i++)
{
print(nums[i]);
}
方法1:
【0, 1, 2, 2, 3, 0, 4, 2】一个数组
给出:val=2
先创造辅助空间 【 】
将元素数组进行遍历,将值不为val的元素搬移到新空间中
将新空间中的元素搬移回原空间
时间复杂夫O(N)
空间复杂度O(N)–>不满足要求
方法2:
先找,然后用顺序表中的一个功能(在任意位置中删除)(将找到的元素后面的所有元素整体向前移动一个位置)
while (-1 != (pos = find(array, size, val)))//找到元素下标然后令pos等于目标元素
erase(array, pos);//删除pos指向的元素
时间复杂度:O(MN)–>M表示值为val的元素个数
虽然合理,但是不是面试官想要的(还有更好的)
方法3:
【0, 1, 2, 2, 3, 0, 2,4, 2】
1.先记录val的个数
2.当val=1时,让val后面一个元素向前移动一个位置
当val=2时,让val后面一个元素向前移动两个位置
…
3.剩余元素就是size-count
int count = 0;//记录值为val的个数
for(int i = 0;i<size;++i)
{
if (array[i] == val)
count++;
else
array[i - coount] = array[i];
}
符合条件
代码
int removeElement(int* nums,int numsSize,int val)
{
int count = 0;//用来记录val的个数
for (int i = 0; i < numsSize; ++i)
{
if (nums[i] == val)
count++;
else
nums[i - count] = nums[i];
}
return numsSize - count;
}
2. 删除排序数组中的重复项。
给定一个排序数组,你需要在 原地 删除重复出现的元素,使得每个元素只出现一次,返回移除后数组的新长度。
不要使用额外的数组空间,你必须在 原地 修改输入数组 并在使用 O(1) 额外空间的条件下完成。
示例 1 :
给定数组 nums = [1, 1, 2],
函数应该返回新的长度 2, 并且原数组 nums 的前两个元素被修改为 1, 2。
你不需要考虑数组中超出新长度后面的元素。
示例 2 :
给定 nums = [0, 0, 1, 1, 1, 2, 2, 3, 3, 4],
函数应该返回新的长度 5, 并且原数组 nums 的前五个元素被修改为 0, 1, 2, 3, 4。
你不需要考虑数组中超出新长度后面的元素。
说明 :
为什么返回数值是整数,但输出的答案是数组呢 ?
请注意,输入数组是以「引用」方式传递的,这意味着在函数里修改输入数组对于调用者是可见的。
你可以想象内部操作如下 :
nums 是以“引用”方式传递的。也就是说,不对实参做任何拷贝
int len = removeDuplicates(nums);
在函数里修改输入数组对于调用者是可见的。
根据你的函数返回的长度, 它会打印出数组中该长度范围内的所有元素。
for (int i = 0; i < len; i++) {
print(nums[i]);
}
题目解释:
【0,0,1,1,1,2,2,3,3,4】
注意:数组是有序的,说明重复性的元素是连在一起的
需求:将重复的元素只保留一个–去重
方法一:
【0,0,1,1,1,2,2,3,3,4】
丨 丨
count i
先给两个标记count i
开始时:count和i相等,此时我们让i向后移一位,此时count=0,i=1
count != i;我们将i指向的元素(1)向前搬移(count+1)个位置,然后count+1
count的功能:记录的是删除后,有效元素的个数
总结一下:count在后面,i在前面.
当i和count相等的时候,i++,count不用管
当i!=count时,i指向的元素向前搬移(count+1)个位置,然后count+1
返回长度时,我们需要返回++count
原因:(元素是01234这样的顺序排列的,直接返回count=4,然而一共有五个元素)
代码初稿:
for (int i = 0; i < size; ++i)
{
if (array[count] != array[i])//则证明i指向的元素是第一次出现的,我们则需要将其想前面搬移
{
//array[count + 1] = array[i];
//count++;
//等同于
array[++count] = array[i];//先让count加一,然后将i指向的元素搬移到count的位置
}
}
代码:
int removeDuplicates(int* nums, int numsSize)
{
int count = 0;//刚开始,count在0号位置,i在1号位置
for (int i = 1; i < numsSize; ++i)// i < numsSize你有多少个元素,后续就需要多少个元素参与比较
{
if (nums[count] != nums[i])
nums[nums[++count] = nums[i]];//++count 注意:count需要先++,然后再进行搬移
}
return ++count;
}
代码不过原因:
当输入的数组为[]
我们的count和i已经占领了两个位置
解决方法:判断数组元素个数是否满足我们的需求
特殊情况:
1.数组没有元素
2.数组只有一个元素,则没有重复的元素
3数组有两个及两个以上,正常运行
正确代码:
int removeDuplicates(int* nums, int numsSize)
{
int count = 0;//刚开始,count在0号位置,i在1号位置
if (numsSize < 2)
return numsSize;
for (int i = 1; i < numsSize; ++i)// i < numsSize你有多少个元素,后续就需要多少个元素参与比较
{
if (nums[count] != nums[i])
nums[nums[++count] = nums[i]];//++count 注意:count需要先++,然后再进行搬移
}
return ++count;
}
3. 合并两个有序数组。
给你两个有序整数数组 nums1 和 nums2,请你将 nums2 合并到 nums1 中,使 nums1 成为一个有序数组。
说明 :
初始化 nums1 和 nums2 的元素数量分别为 m 和 n 。
你可以假设 nums1 有足够的空间(空间大小大于或等于 m + n)来保存 nums2 中的元素。
示例 :
输入 :
nums1 = [1, 2, 3, 0, 0, 0], m = 3//有六个空间,但是只占领了三个空间
nums2 = [2, 5, 6], n = 3//有三个空间,占领了三个空间
输出 : [1, 2, 2, 3, 5, 6]
方法一:采用辅助空间
1.申请辅助空间:申请m+n个空间
2.将nums1和nums2中的元素向新空间中搬移:每次搬移一个,搬移两个数组中较小的一个元素
nums[1]和nums[2]进行比较,较小的搬移至新空间,然后将较小的 +1,较大的不变
然后继续比较
当nums1中的元素全部搬移完后此时m>3,我们将nums2中剩余的所有元素搬移至新空间
3.将新空间归并好的元素拷贝到nums1
4.释放新空间
5.返回
时间复杂度:O(M+N)
空间复杂度:O(M+N)
方法二:不适用辅助空间(将nums2中的元素诸葛插入到nums1中合适的位置)
1.将nums1中的元素和nums2元素进行比较
2.当nums1的元素小于nums2元素,nums1++,nums2不变
当nums1的元素大于nums2元素,将nums2中的元素按照(从任意位置中插入的方法插入)
(将nums1中的元素向后移动一位,将nums2插入进去),然后nums1++,nums2++
时间复杂度:O(MN)
空间复杂度:O(1)
方法三:从后往前进行比较
【1,2,3,0,0,0,】【2,5,6】
丨 丨 丨
i index j
用i和j进行比较然后大的等于index,然后大的往后走一位,小的不动,index往后走一步
时间复杂度:O(M+N)
空间复杂度:0(1)
错误代码:
void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n)
{
int index = m+n - 1;//最大元素在nums1中存储的位置
//n>0:表示nums2中还有元素没有搬移到nums1中
while (n>0)
{
if (nums1[m - 1] > nums2[n - 1])
{
nums1[index--] = nums1[m-1];
m--;
}
else
{
nums2[index--] = nums2[n - 1];
n--;
}
}
}
void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n)
中有六个参数,而我们错误后的,输入示例只有四个参数,我们此时可以对照题目中的
示例 :
输入 :
nums1 = [1, 2, 3, 0, 0, 0], m = 3//有六个空间,但是只占领了三个空间
nums2 = [2, 5, 6], n = 3//有三个空间,占领了三个空间
输出 : [1, 2, 2, 3, 5, 6]
题目这一块则向我们证明了系统输入的含义即:
当输入:[0] 0 [0]代表nums1数组,他有 0 个元素
[1] 1 [1]代表nums2数组,他有 1 个元素
正确代码:
void merge(int* nums1, int nums1Size, int m, int* nums2, int nums2Size, int n)
{
int index = m+n - 1;//最大元素在nums1中存储的位置
//n>0:表示nums2中还有元素没有搬移到nums1中
while (n>0)
{
if (m > 0 && nums1[m - 1] > nums2[n - 1])
{
nums1[index--] = nums1[m - 1];
m--;
}
else
{
nums1[index--] = nums2[n - 1];
n--;
}
}
}
4. 旋转数组。
方法:
例如:k=3;数组为1,2,3,4,5,6,7
我们令1,2,3,4为(1)
5,6,7为(2)
1,2,3,4,5,6,7整体为(3)
1:对(1)先进行逆转:4 3 2 1
2:对(2)逆置:7 6 5
此时:4 3 2 1 7 6 5
3:对整体进行你逆转(逆置)(前后对调)
5 6 7 1 2 3 4
void reverse(int *nums, int start, int end)
{
int temp;
while (start < end)
{
temp = nums[start];
nums[start] = nums[end];
nums[end] = temp;
start++;
end--;
}
}
void rotate(int *nums, int numsSize, int k)
{
k %= numsSize;
reverse(nums, 0, numsSize - 1);
reverse(nums, 0, k - 1);
reverse(nums, k, numsSize - 1);
}
5. 数组形式的整数加法。
对于非负整数 X 而言,X 的数组形式是每位数字按从左到右的顺序形成的数组。例如,如果 X = 1231,那么其数组形式为 [1, 2, 3, 1]。
给定非负整数 X 的数组形式 A,返回整数 X + K 的数组形式。
示例 1:
输入:A = [1, 2, 0, 0], K = 34
输出:[1, 2, 3, 4]
解释:1200 + 34 = 1234
示例 2:
输入:A = [2, 7, 4], K = 181
输出:[4, 5, 5]
解释:274 + 181 = 455
示例 3:
输入:A = [2, 1, 5], K = 806
输出:[1, 0, 2, 1]
解释:215 + 806 = 1021
示例 4:
输入:A = [9, 9, 9, 9, 9, 9, 9, 9, 9, 9], K = 1
输出:[1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
解释:9999999999 + 1 = 10000000000
c语言无法提供的内置类型无法表示上述类型(9999999999999+9999999999)
则不能对其进行四则混合运算
方法:将大数按照每一位保存在数组中,用数组来模拟四则运算
提示:
1 <= A.length <= 10000
0 <= A[i] <= 9
0 <= K <= 10000
如果 A.length > 1,那么 A[0] != 0
问题一:运算出的结果我们应该开辟多大的空间
举例说明:
A:[2 3] k:10000-----5
A:[2 3] k:9999-----5
A:[999999999] k:10-----10
A:[2 3] k:999-----4
保存结果的空间的大小:应该比较长数据位数多一位,因为加完之后最后可能还有一个进位
用数组中的每一位加k
示例 3:
输入:A = [2, 1, 5], K = 806
输出:[1, 0, 2, 1]
解释:215 + 806 = 1021
步骤:
(1)806+5= 811 811%10—>1 811/10—>81
(2)81+1=82 82%10—>2 82/10---->8
(3)8+2=10 10%10—>0 10/10—>1
(4)1+0=1 1%10—>1 1/10—>0
代码实现:
**int* addToArrayForm(int* A, int ASize, int K, int* returnSize)
{
int size = ASize > 5 ? ASize+1 : 5+1;
//在提示中:1 <= A.length <= 10000
// 0 <= K <= 10000
//k最多是5位,而A的长度是1 <= A.length <= 10000
int* ret = calloc(size, sizeof(int)); //用来返回的所需空间
//表示加完之后数据的总位数
int total = 0;
//用数组的每一位加k
int retIdx = size - 1;
while (ASize > 0)//说明数组里面有元素 //可以保证数组中的元素被加完,K中还有剩余数据
{
K += A[ASize - 1]; //数组最低位
ret[retIdx--] = K % 10;
K /= 10;
total++;
ASize--;
}
while (K > 0)
{
ret[retIdx--] = K % 10;
K /= 10;
total++;
}
//结果没有超过数组,K的位数大小,则需要搬移
//原因:我们申请数组结果的空间大小是多一位的(一般情况)
// 我们最小申请六位空间(当结果小于5位时,则需要搬移2-4位)
if (total < size)
{
// 搬移至 搬移量 搬移多少位
memmove(ret,ret+(size-total),total*sizeof(int));
}
*returnSize = total;
return ret;
}**
顺序表缺陷:在其首部或者任意位置插入||删除时需要搬移大量的元素—顺序表不适合做大量的插入和删除操作
原因:其底层是一段连续的空间
2.4 顺序表的问题及思考
问题:
- 中间 / 头部的插入删除,时间复杂度为O(N)
- 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
- 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到200,
我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。
思考:
如何解决以上问题呢?下面给出了链表的结构来看看。
链表:
【1】 【2】 【3】 【4】
将其分开存储,此时我们便有疑问?我们怎么知道【1】后面的元素是谁?
我们再【1】里面保存【2】的地址
例如:
0x1234 0x2345 0x3456 0x4567
【1】 【2】 【3】 【4】
0x2345 0x3456 0x4567 NULL
这种线性表的优点:
例如:我们要在【2】和【3】中间插入一个元素【0】(【0】地址:0x5678)
1.我们先找一个空间将【0】保存起来
0x1234 0x2345 0x5678 0x3456 0x4567
【1】 【2】 【0】 【3】 【4】
0x2345 0x5678 0x3456 0x4567 NULL
丨->循环链表:
丨
丨-> 不带头节点:--丨->不循环链表:
丨->单链表:存储元素 --丨
丨 下一个节点的地址 丨
丨 丨-> 带头节点:----丨->循环链表:
丨 丨
丨 丨->不循环链表:
链表分类:-丨
丨 丨->循环链表:
丨 丨
丨 丨-> 不带头节点:–丨->不循环链表:
丨->双向链表:存储元素 --丨
下一个节点的地址 丨
上一个节点的地址 丨-> 带头节点:----丨->循环链表:
丨
丨->不循环链表:
共8种链表
单链表:不带头节点,带头节点
存储元素以及下一个节点的地址
1->2->3->4->NULL
不带头节点的单链表:循环链表,不循环链表
不循环链表:
0x1234 0x2345 0x3456 0x4567
【1】 【2】 【3】 【4】
0x2345 0x3456 0x4567 NULL
循环链表:
0x1234 0x2345 0x3456 0x4567
【1】 【2】 【3】 【4】
0x2345 0x3456 0x4567 0x1234
带头节点的单链表://其中第一个【】中不存放有效数据(区分)
0x1234 0x2345 0x3456 0x4567
【 】 【1】 【2】 【3】 【4】
0x1234 0x2345 0x3456 0x4567 NULL
双向链表:存储元素,下一个节点的地址,上一个节点的地址
NULL<-1<=>2<=>3<=>4->NULL
注意两种链表:
不带头节点的单链表:面试期间经常考–没有明确说明时指的都是不带头节点的单链表
带头节点的双向循环链表:–在实践中应用比较多
不带头节点的单链表:
先创造一个头文件SList.h
链表中存放的都是一个一个的节点,节点里面需要存放一个值域以及指向下一个节点的地址
问题?c语言中有什么复合类型,它可以存放值域和地址的?–没有
既然没有,那么我们则需要将这种类型用struct定义出来
模拟单链表
https://blog.csdn.net/sakeww/article/details/107389733
3.3 链表面试题
- 删除链表中等于给定值 val 的所有节点。
示例:
输入 : 1->2->6->3->4->5->6, val = 6
输出 : 1->2->3->4->5
题目延伸:一个链表里面不一定只有一个6
6->6->1->2->6->3->4->5->6
即:1.首节点如果是val—》头删思想
2.其他节点是val
prev:cur的前一个节点
cur:当前正在检测的节点
struct ListNode* removeElements(struct ListNode* head, int val)
{
struct ListNode* cur = head;
struct ListNode* prev = NULL;
while (cur)
{
if (cur->val == val)
{
//该节点要被删除掉
if (cur == head)
{
//cur为第一个节点,采用头删方式删除
head = cur->next;
free(cur);
cur = head;
}
else
{
//cur不是第一个节点
prev->next = cur->next;
free(cur);
cur = prev->next;
}
}
else
{
prev = cur;
cur = cur->next;
}
}
return head;
}
2. 反转一个单链表。
示例:
输入 : 1->2->3->4->5->NULL
输出 : 5->4->3->2->1->NULL
进阶 :
你可以迭代或递归地反转链表。你能否用两种方法解决这道题?
三个指针
typedef struct listNode listNode;
struct ListNode* reverseList(struct ListNode* head)
{
ListNode* prev = NULL;
ListNode* cur = head;
ListNode* next = NULL;
while (cur)
{
next = cur->next;
cur->next = prev;
prev = cur;
cur = next;
}
return prev;
}
采用头插法
将head中的元素一个一个的用头插法插入新的链表中
typedef struct listNode listNode;
struct ListNode* reverseList(struct ListNode* head)
{
ListNode* newhead = NULL;
ListNode* cur = head;
ListNode* next = NULL;
while (cur)
{
next = cur;
cur->next = newhead;
newhead = cur;
cur = next;
}
return prev;
}
迭代方法
记录链表中的三个连续节点:reverse, first, second。在每轮迭代中,从原链表中提取first并将它插入逆链表的开头。一直保持first指向原链表剩余节点的首节点,second指向原链表剩余节点的第二个节点,reverse指向逆链表的首节点
public Node reverse(Node x)
{
Node first = x;
Node reverse = null;
while (first != null)
{
Node second = first.next;
first.next = reverse;
reverse = first;
first = second;
}
return reverse;
}
递归方法
假设含有N个节点,先递归颠倒最后N - 1个节点,然后将链表的首节点插入到结果的链表的末端
public Node reverse(Node first)
{
if (first == null)
return null;
if (first.next != null)
return first;
Node second = first.next;
Node rest = reverse(second)
second.next = first;
first.next = null;
return rest;
}
- 给定一个带有头结点 head 的非空单链表,返回链表的中间结点。如果有两个中间结点,则返回第二个中间结点。
示例 1:
输入:[1, 2, 3, 4, 5]
输出:此列表中的结点 3 (序列化形式:[3, 4, 5])
返回的结点值为 3 。(测评系统对该结点序列化表述是[3, 4, 5])。
注意,我们返回了一个 ListNode 类型的对象 ans,这样:
ans.val = 3, ans.next.val = 4, ans.next.next.val = 5, 以及 ans.next.next.next = NULL.
示例 2:
输入:[1, 2, 3, 4, 5, 6]
输出:此列表中的结点 4 (序列化形式:[4, 5, 6])
由于该列表有两个中间结点,值分别为 3 和 4,我们返回第二个结点。
提示:
给定链表的结点数介于 1 和 100 之间。
题目隐含信息:链表个数未知
方法一:先算出链表内元素个数,除以二,然后从头找到这个节点,然后,,,,
方法二:两个节点fast和slow,fast每次走两步,slow每次走一步,当fast走到末尾时,此时从slow截取
面试中,一般会要求你的链表只能遍历一遍,因而方法一不合适
struct ListNode* middleNode(struct ListNode* head)
{
struct ListNode* fast = head;
struct ListNode* slow = head;
while (fast && fast->next)
{
fast = fast->next->next;
slow = slow->next;
}
return slow;
}
延申:当链表个数为偶数个时,上面时取中间两个中后一个,如果要取前一个应如何?
struct ListNode* middleNode(struct ListNode* head)
{
struct ListNode* fast = head;
struct ListNode* slow = head;
struct ListNode* preslow = head;
while (fast && fast->next)
{
fast = fast->next->next;
preslow = slow;
slow = slow->next;
}
if (fast)
return slow;
else
return preslow;
}
- 输入一个链表,输出该链表中倒数第k个结点。
要求:只能遍历一次链表
方法:(1)让fast先向后走k步,(2)再让slow与fast同时往后移动,直到fast走到链表末尾,slow所指向的节点即为所求
隐藏考点:假如链表只有5个元素,此时需要你求倒数第6个,则没有这个元素–》(1)过程运行时,对fast进行判
class Solution
{
public:
ListNode* FindKthToTail(ListNode* pListHead, unsigned int k)
{
if (NULL == pListHead || k == 0)
return NULL;
ListNode* fast = pListHead;
ListNode* slow = pListHead;
//(1)
while (k--)
{
//检测k是否大于链表中节点的个数
if (NULL == fast)
return 0;
fast = fast->next;
}
//(2)
while (fast)
{
fast = fast->next;
slow = slow->next;
}
return slow;
}
};
- 将两个有序链表合并为一个新的有序链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
示例:
输入:1->2->4, 1->3->4
输出:1->1->2->3->4->4
方法:将链表1和链表2中的每一个元素依次进行比较,将小的元素依次往新链表中进行尾插
typedef struct ListNode listNode;
struct ListNode* mergeTwoLists(struct ListNode* l1, struct ListNode* l2)
{
if (NULL == l1)
return l2;
if (NULL == l2)
return l1;
//l1和l2均不为空
ListNode* newhead = NULL;
ListNode* cur1 = l1;
ListNode* cur2 = l2;
if (cur1->val <= cur2->val)//--------1
{
newhead = cur1;
tailNode = cur1;
cur1 = cur->next;
}
else
{
newhead = cur2;
tailNode = cur2;
cur2 = cur2->next;
}
while (cur1 && cur2)//----------2
{
if (cur1->val1 <= cur2->val)
{
tailNode->next = cur1;
cur1 = cur1->next;
}
else
{
tailNode->next = cur2;
cur2 = cur2->next;
}
tailNode = tailNode->next;
}
if (cur1)
tailNode->next = cur1;
else
tailNode->next = cur2;
return newhead;
}
1和2 里面有些内容有点重复
修改一下
typedef struct ListNode listNode;
struct ListNode* mergeTwoLists(struct ListNode* l1, struct ListNode* l2)
{
if (NULL == l1)
return l2;
if (NULL == l2)
return l1;
//l1和l2均不为空
ListNode* cur1 = l1;
ListNode* cur2 = l2;
ListNode newhead;
ListNode* tailNode = &newhead;
while (cur1 && cur2)
{
if (cur1->val1 <= cur2->val)
{
tailNode->next = cur1;
cur1 = cur1->next;
}
else
{
tailNode->next = cur2;
cur2 = cur2->next;
}
tailNode = tailNode->next;
}
if (cur1)
tailNode->next = cur1;
else
tailNode->next = cur2;
return newhead.next;
}
带头节点的链表来进行优化
链表中第一个节点中是否存放有效数据,如果不存放有效数据,则为带头节点的链表:如果存放有效数据,则为不带头节点的链表。
带头节点的链表插入和删除操作比不带头节点的链表简单
带头节点的单链表
#include<stdio.h>
#include<malloc.h>
typedef struct SHListNode
{
int data;
struct SHListNode* next;
}SHListNode;
void SHListPushBack(SHListNode* head, int data)
{
SHListNode* newNode = (SHListNode*)malloc(sizeof(SHListNode));
newNode->next = NULL;
newNode->data = data;
SHListNode* tailNode = head;
while (tailNode->next)
{
tailNode = tailNode->next;
}
tailNode->next = newNode;
}
void TestHList()
{
SHListNode headNode;//链表的头节点
headNode.next = NULL;
SHListNode(&headNode, 1);
SHListNode(&headNode, 2);
SHListNode(&headNode, 3);
SHListNode(&headNode, 4);
SHListNode(&headNode, 5);
}
int main()
{
TestHList();
return 0;
}
问题:带头节点的链表,头结点的值域中没有存放有效数据, 能否将链表的长度即链表中有效节点的个数存储到头结点的值域中呢?
带头节点的单链表与不带头节点的单链表区分:
(1)第一个节点是否保存有效数据,如果第一个节点没有保存有效数据则为带头节点的链表,否则就是不带头节点的链表
(2)带头节点链表操作更加简单—不需要传递二级指针
问题:既然带头节点的链表中第一个节点中不保存有效数据,能否将链表的长度存储到头结点的值域中?
答:不行
struct ListNode
{
struct LisrNode* next;
字段:存储节点中的数据//我们以后在这个链表存储什么类型的数据,我们就把这个节点给成什么类型(这个需要根据应用场景来确定)
整形:int-----------整形有自己的范围,如果里面的数据超过这个范围,我们就表示不了
字符:char----------同整形一样有一个范围【-128,127】,如果需要往里面存储1000个数据,我们将1000这个数据放到里面就会被截断
结构体:结构体
};
我的理解:字段这一行代码中,如果我们往这里存储一个数字,用来表示这个链表的长度,我们应该用那个类型来表示?
每一个类型都有自己的上限和下限,例如:char只能表示-128到127,int只能表示,,,我们无法找到一个能够表示的这个数字的类型,所以不行
况且这一行也要表示这个链表中数据的类型
有些书本里面这一行会表示链表中元素的个数,但是我们要清楚,这种做法不可取,一旦链表里面的元素多了,这样表示就不准确了
- 编写代码,以给定值x为基准将链表分割成两部分,所有小于x的结点排在大于或等于x的结点之前 。
暗藏信息:给定的这个链表是无序的
输入:2->7->8->4->3->6->5->1->NULL x= 5
输出:2-4->3->1->5->7->8->6->5
方法:将上述链表分割成两个链表,一个链表中存储小于x的节点,一个链表中存储大于等于x的节点,然后将大的链表连接到小的链表后面
思路:
对原链表进行遍历,拿到一个节点cur,将该节点从原链表中移除出来
如果cur->val < x ,则将其插入到head1
否则将cur尾插到head2
class Partition
{
public:
ListNode* partition(ListNode* pHead, int x)
{
if (NULL == pHead)
return NULL;
ListNode lessxHead(0);
ListNode* lessTail = &lessxHead;
ListNode greatxHead(0);
ListNode* greatTail = &greatxHead;
ListNode* cur = pHead;
while (cur)
{
pHead = cur->next;
//将cur节点尾插到lessxHead || greatxHead
if (cur->val < x)
{
lessxTail->next = cur;
lessTail = cur;
}
else
{
greatTail->next = cur;
greatTail = cur;
}
cur = pHead;
}
greatTail->next = NULL;
lessTail->next = greatxHead->next;
return lessxHead->next;
}
};
- 链表的回文结构。
题目描述:
对于一个链表,请设计一个时间复杂度为O(n), 额外空间复杂度为O(1)的算法,判断其是否为回文结构。
给定一个链表的头指针A,请返回一个bool值,代表其是否为回文结构。保证链表长度小于等于900。
测试样例:
1->2->2->1
返回:true
借助辅助空间
class PalindromeList
{
public:
bool chkPalindrome(ListNode* A)
{
if (NULL == A)
return true;
int array[900] = { 0 };
int size = 0;
//将链表中的节点放置到array的数组中
ListNode*cur = A;
while ()
{
array[size++] = cur->val;
cur = cur->next;
}
int left = 0, right = size - 1;
while (left < right)
{
if (array[left] != srray[right])
return false;
left++;
right--;
}
return true;
}
};
不借助辅助空间
1.找到原链表的中间位置mid
2.将mid及其后续所有的节点进行逆置
3.检测两个链表中节点值域是否一致
class PalindromeList
{
public:
ListNode* ReverseList(ListNode* head)
{
ListNode* cur = head;
LIstNode* prev = NULL;
ListNode* next = NULL;
while (cur)
{
next = cur->next;
cur->next = prev;
prev = cur;
cur = next;
}
return prev;
}
bool chkPalindrome(ListNode* A)
{
if (NULL == A)
return0;
//找链表的中间节点
ListNode* fast = A;
ListNode* slow = A;
while (fast&&fast->next)
{
fast = fast->next->next;
prevSlow = slow;
slow = slow->next;
}
//中间按节点为slow
//将中间节点及其后续所有的节点进行逆置
ListNode* rightHead = ReverseList(slow);
ListNode* curLeft = A;
ListNode* curRight = rightHead;
while (curLeft&&curRight)
{
if (curLeft->Val != curRight->val)
return false;
curLeft = curLeft->next;
curRight = curRight->next;
}
//需要将原链表还原
ReverseList(rightHead)
return true;
}
};
- 输入两个链表,找出它们的第一个公共结点。
编写一个程序,找到两个单链表相交的起始节点。
如下面的两个链表:
在节点 c1 开始相交。
示例 1:
输入:intersectVal = 8, listA = [4, 1, 8, 4, 5], listB = [5, 0, 1, 8, 4, 5], skipA = 2, skipB = 3
输出:Reference of the node with value = 8
输入解释:相交节点的值为 8 (注意,如果两个链表相交则不能为 0)。从各自的表头开始算起,链表 A 为[4, 1, 8, 4, 5],链表 B 为[5, 0, 1, 8, 4, 5]。在 A 中,相交节点前有 2 个节点;在 B 中,相交节点前有 3 个节点。
示例 2:
输入:intersectVal = 2, listA = [0, 9, 1, 2, 4], listB = [3, 2, 4], skipA = 3, skipB = 1
输出:Reference of the node with value = 2
输入解释:相交节点的值为 2 (注意,如果两个链表相交则不能为 0)。从各自的表头开始算起,链表 A 为[0, 9, 1, 2, 4],链表 B 为[3, 2, 4]。在 A 中,相交节点前有 3 个节点;在 B 中,相交节点前有 1 个节点。
示例 3:
输入:intersectVal = 0, listA = [2, 6, 4], listB = [1, 5], skipA = 3, skipB = 2
输出:null
输入解释:从各自的表头开始算起,链表 A 为[2, 6, 4],链表 B 为[1, 5]。由于这两个链表不相交,所以 intersectVal 必须为 0,而 skipA 和 skipB 可以是任意值。
解释:这两个链表不相交,因此返回 null。
注意:
如果两个链表没有交点,返回 null.
在返回结果后,两个链表仍须保持原有的结构。
可假定整个链表结构中没有循环。
程序尽量满足 O(n) 时间复杂度,且仅用 O(1) 内存。
找到链表的交点?
1.确定两个链表是否相交------》找到两个链表中的最后一个节点,然后检测最后一个节点的地址是否相同
2.求交点:第一个公共的节点
如果链表相交,从交点到链表中最后一个节点都是两个链表中公共的节点
typedef struct listNode ListNode;
struct ListNode *getIntersectionNode(struct ListNode *headA, struct ListNode *headB)
{
//两个链表只要有一个为空,则不可能相交
if (NULL == headA || NULL == headB)
return NULL;
//两个链表都不为空
//1.两个链表是否相交:找到链表中的最后一个节点,检测该节点的地址是否相同即可
ListNode* curA = headA;
int countA = 1;
while (curA->next)
{
count++;
curA = curA->next;
}
ListNode* curB = headB;
int countB = 1;
while (curB->next)
{
countB++;
curB = curB->next;
}
//检测两个链表中最后一个节点的地址是否相同,不相同则不相交
if (curA != curB)
return NULL;
//求交点
//让长的链表从起始位置往后走gap步
curA = headA;
curB = headB;
int gap = countA - countB;
if (gap > 0)
{
while (gap--)
curA = curA->next;
}
else
{
while (gap++)
{
curB = curB->next;
}
}
//不断检测curA和curB是否地址相同
while (curA != curB)
{
curA = curA->next;
curB = curB->next;
}
return curA;
}
- 给定一个链表,判断链表中是否有环。
为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 - 1,则在该链表中没有环。
示例 1:
输入:head = [3, 2, 0, -4], pos = 1
输出:true
解释:链表中有一个环,其尾部连接到第二个节点。
示例 2:
输入:head = [1, 2], pos = 0
输出:true
解释:链表中有一个环,其尾部连接到第一个节点。
示例 3:
输入:head = [1], pos = -1
输出:false
解释:链表中没有环。
进阶:
你能用 O(1)(即,常量)内存解决此问题吗?
方法:两个指针,一个每次走两步,一个一次走一步,如果两个相遇,则为环,如果不相遇则不为环
bool hasCycle(struct ListNode *head)
{
struct ListNode* fast = head;
struct ListNode* slow = head;
//链表可能带环,也可能不带环
//如果不带环,fast指针可能先走到链表的末尾
while (fast && fast->next)
{
fast = fast->next->next;
slow = slow->next;
if(fast == slow)
return true;
}
return false;
}
为什么?
两个指指针每走一次,两个指针之间间隔的节点个数在减少一个–而环最小的长度(环中节点的个数)是一
面试官追问:如果让快指针走3或者4步,慢指针每次走一步,能否判断链表带环?
不行
- 给定一个链表,返回链表开始入环的第一个节点。 如果链表无环,则返回 NULL
结论:给一个指针pH从链表的第一个节点开始移动,给一个指针pM从判环相遇点的位置绕环,ph和pM在入口点的位置相遇
为了表示给定链表中的环,我们使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。 如果 pos 是 - 1,则在该链表中没有环。
说明:不允许修改给定的链表。
示例 1:
输入:head = [3, 2, 0, -4], pos = 1
输出:tail connects to node index 1
解释:链表中有一个环,其尾部连接到第二个节点。
示例 2:
输入:head = [1, 2], pos = 0
输出:tail connects to node index 0
解释:链表中有一个环,其尾部连接到第一个节点。
示例 3:
输入:head = [1], pos = -1
输出:no cycle
解释:链表中没有环。
进阶:
你是否可以不用额外空间解决此题?
typedef struct ListNode ListNode
ListNode* hasCycle(ListNode* head)
{
//检测链表是否带环
ListNode* fast = head;
ListNode* slow = head;
while (fast && fast->next)
{
fast = fast->next->next;
slow = slow->next;
if (fast == slow)
return fast;
}
}
struct ListNode *detectCycle(struct ListNode *head)
{
ListNode* pM = hasCycle(head);
//两个指针不相等时则不带环
if (fast != slow)
return NULL;
ListNode* pH = head;
while ()
{
pH = pH->next;
pM = pM->next;
}
return pM;
}
前提条件:一个带环链表
H<----L------->E<-----X----->M
| |
-------------------
<---------r------->
令X+r= R
判环时(两个指针一快一慢最终会在环内相遇):
慢指针走的长度:L+X
快指针走的长度:L+X+nR (n属于整数)
可以推出等式:
2*(L+X)=L+X+nR------》L = nR - X
结论:求环的入口点:给两个指针PH和PM
PH从链表起始节点开始,PM从相遇点的位置开始遍历链表,两个指针每次各走一步最终PH和PM肯定会在入口点的位置相遇
环外相交:交点在环外 --》求交点
方式一:借助不带环链表相交求交点的方式
将相遇点PM认为是L1和L2的终点
L1到PM及L2到PM位置节点的个数size1和size2
然后给一个两个指针P1和P2,分别指向L1和L2首节点的位置
让长节点个数往后移动差值步,最后两个指针同时往后移动
结果肯定会在交点的位置相遇
方式二:
1.PM的next不要指向PMNext,rang PM的next指向两个链表中任意一个链表的首节点,比如L2
2.链表中形成了一个新的环–》以前的交点现在成了新环的入口点
3.求原L1和L2的交点E,就变成求新环的入口点
如何判断相交?
判环期间得到两个相遇点,如果相交则两个相遇点在同一环中:给一个指针让其从一个相遇点的位置绕环一周,检测是否遇到另外一个相遇点
环内相交:交点在环上
- 给定一个链表,每个节点包含一个额外增加的随机指针,该指针可以指向链表中的任何节点或空节点。要求返回这个链表的深度拷贝。
我们用一个由 n 个节点组成的链表来表示输入 / 输出中的链表。每个节点用一个 [val, random_index] 表示:
val:一个表示 Node.val 的整数。
random_index:随机指针指向的节点索引(范围从 0 到 n - 1);如果不指向任何节点,则为 null 。
示例 1:
输入:head = [[7, null], [13, 0], [11, 4], [10, 2], [1, 0]]
输出:[[7, null], [13, 0], [11, 4], [10, 2], [1, 0]]
示例 2:
输入:head = [[1, 1], [2, 1]]
输出:[[1, 1], [2, 1]]
示例 3:
输入:head = [[3, null], [3, 0], [3, null]]
输出:[[3, null], [3, 0], [3, null]]
示例 4:
输入:head = []
输出:[]
解释:给定的链表为空(空指针),因此返回 null。
提示:
- 10000 <= Node.val <= 10000
Node.random 为空(null)或指向链表中的节点。
节点数目不超过 1000 。
新链表中每个节点的随机指针域赋值:
1.如果原链表中节点随机指针域指向空或者自己,也让新链表当前节点随机指针域指向NULL或者自己
2.如果原链表中节点随机指针域指向其他节点
》》求原链表中当前节点的随机指针域到原链表起始位置的距离dist
》》给一个指针p让其指向新链表中节点,让p从新链表起始位置往后移动dist步之后指向的节点
即新链表中当前节点的随机指针域该指向的节点
方法一:
1.将原链表拷贝一份,再给每个指针的随机指针域赋值
方式二:
1.在原链表每个节点之后插入值相等的新节点
2.给新插入节点的随即指针域进行赋值
3.将新插入的节点从原链表中拆下来
typedef struct Node Node;
Node* BuyRandomListNode(int val)
{
Node* newNode = (Node*)malloc(sizeof(Node));
if (NULL == newNode)
return NULL;
newNode->val = cur->val;
newNode->next = NULL;
newNode->random = NULL;
return newNode;
}
struct Node* copyRandomList(struct Node* head)
{
if (NULL == head)
return NULL;
//1.在原链表中每个节点后插入值相等的新节点
Node* cur = head;
Node* newNode = NULL;
while (cur)
{
newNode = BuyRandomListNode(cur->val)
if (NULL == newNode)
return NULL;
newNode->next = cur->next;
cur->next = newNode;
cur = newNode->next;
}
//2.给新插入节点的随即指针域进行赋值
cur = head;
while (cur)
{
newNode = cur->next;
if (cur->random)
newNode->random = cur->random->next;
cur = newNode->next;
}
//3.将新节点从原链表中拆下来
Node* newHead = head->next;
cur = head;
while (cur->next)
{
newNode = cur->next;
cur->next = newNode->next;
cur = newNode;
}
return newHead;
}
12 . 对链表进行插入排序。
插入排序算法:
插入排序是迭代的,每次只移动一个元素,直到所有元素可以形成一个有序的输出列表。
每次迭代中,插入排序只从输入数据中移除一个待排序的元素,找到它在序列中适当的位置,并将其插入。
重复直到所有输入数据插入完为止。
示例 1:
输入 : 4->2->1->3
输出 : 1->2->3->4
示例 2:
输入 : -1->5->3->4->0
输出 : -1->0->3->4->5
struct ListNode* insertionSortList(struct ListNode* head)
{
//链表为空或者链表中只有一个节点
if (NULL == head || NULL == head->next)
return head;
//链表中至少有两个节点
//从原链表中获取一个节点将其插入到新链表当中
struct ListNode* newHead = NULL;
struct ListNode* cur = head;
while (cur)
{
//cur即为需要向新链表中插入的节点
head = cur->next;
//新链表中有节点,找新插入节点的位置
struct ListNode* insertPos = newHead;
struct ListNode* insertPosPrev NULL;
while (insertPos)
{
if (cur->val > insertPos->val)
{
insertPosPrev = insertPos;
insertPos = insertPos->next;
}
else
{
break;
}
}
//cur比新链表中所有的节点都小--头插
if (NULL == insertPosPrev)
{
//头插
cur->next = newHead;
newHead = cur;
}
else
{
//cur节点中的数据比insertPos节点中的数据小
cur->next = insertPos;
insertPosPrev->next = cur;
}
cur = head;
}
return newHead;
}
//原理:依次从原链表中取节点,将其插入到新链表中
//1.从原链表中取一个节点
//2.将该节点从原链表张删除
//3.在新链表中找cur的插入位置
//4.将cur节点插入到新链表中pos位置之前
- 在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。
在一个排序的链表中,存在重复的结点,请删除该链表中重复的结点,重复的结点不保留,返回链表头指针。 例如,链表1->2->3->3->4->4->5 处理后为 1->2->5
class Solution
{
public:
ListNode* deleteDuplication(ListNode* pHead)
{
ListNode* start = pHead;
ListNode* end = NULL;
ListNode* prev = NULL;
while (start)
{
end = start->next;
//找重复节点的范围
while (end)
{
if (start->val != end->val)
break;
end = end->next;
}
//[start,end)区间中的节点删除掉
if (start->next == end)
{
//区间中没有重复的元素
prev = start;
start = end;
}
else
{
//[stert,end)有重复的节点
while (start != end)
{
//头删
if (start == pHead)
{
head = start->next;
free(shart);
start = head;
}
else
{
//其他节点的删除方式
prev->next = start->next;
free(start);
start = prev->next;
}
}
}
}
return pHead;
}
};
1.找到重复节点区间范围[start,end)
2.删除重复区间中的节点[start,end)
2、带头+双向+循环链表增删查改实现
https://blog.csdn.net/sakeww/article/details/108071113
4.顺序表和链表区别与联系:
共同点:
1.都是线性表
2.元素逻辑存储上是连续的(注意:说的不是元素大小有序,指的是逻辑存储)
3.每个元素都有唯一的前驱以及唯一的后继(注意:第一个元素没有前驱,最后一个元素没有后继–循环链表除外)
不同点:
1.底层存储空间不同:顺序表底层空间是连续的 链表底层空间不连续
2.插入和删除方式不同:顺序表任意位置插入和删除需要搬移大量的元素,效率低,时间复杂度为O(N)
链表任意位置插入和删除不需要搬移元素,效率高,时间复杂度为O(1)
3.随机访问:顺序表底层空间连续,支持随机访问,访问任意位置元素时间复杂度为O(1)
链表底层空间不连续,不支持随机访问,访问任意位置元素时间复杂度为O(N)
4.扩容:顺序表在插入时候需要进行扩容–开辟新空间,拷贝元素,释放旧空间
链表:插入时候不需要进行扩容
5.空间利用率:一般情况顺序表空间利用率比较高
链表中的元素存储在一个一个的节点中,而每个节点都是malloc出来的:频繁向堆申请小的内存块–内存碎片,效率低
6.应用场景不同:顺序表适合:元素高校存储以及随机访问操作比较多的场景
链表:适合任意位置插入和删除比较频繁的场景
7.缓存利用率:链表的缓存利用率比顺序表低
5.顺序表和链表概念选择题
概念选择题:
1.在一个长度为n的顺序表中删除第i个元素,要移动_______个元素。如果要在第i个元素前插入一个元素,要后
移_________个元素。 A
A n - i,n - i + 1
B n - i + 1,n - i
C n - i,n - i
D n - i + 1,n - i + 1
注意:第二问是第i个元素 前 插入
2.取顺序表的第i个元素的时间同i的大小有关(B)
A 对
B 错
应该是和i的位置
3.在一个具有 n 个结点的有序单链表中插入一个新结点并仍然保持有序的时间复杂度是(B)。
A O(1)
B O(n)
C O(n2)
D O(nlog2n)
4.下列关于线性链表的叙述中,正确的是(C )。
A 各数据结点的存储空间可以不连续,但它们的存储顺序与逻辑顺序必须一致
B 各数据结点的存储顺序与逻辑顺序可以不一致,但它们的存储空间必须连续
C 进行插入与删除时,不需要移动表中的元素
D 以上说法均不正确
5.设一个链表最常用的操作是在末尾插入结点和删除尾结点,则选用(D)最节省时间。
A 单链表
B 单循环链表
C 带尾指针的单循环链表
D 带头结点的双循环链表
6.链表不具有的特点是(C)。
A 插入、删除不需要移动元素
B 不必事先估计存储空间
C 可随机访问任一元素
D 所需空间与线性表长度成正比
7.在一个单链表中,若删除 P 所指结点的后续结点,C则执行 ?
A p = p->next;p->next = p->next->next;
B p->next = p->next;
C p->next = p->next->next;
D p = p->next->next
8.一个单向链表队列中有一个指针p,现要将指针r插入到p之后,该进行的操作是__C__。
A p->next = p->next->next
B r->next = p; p->next = r->next
C r->next = p->next; p->next = r
D r = p->next; p->next = r->next
E r->next = p; p->next = r
F p = p->next->next