目录
一、线性表
概念:线性表就是n个相同特性的数据元素的有限序列。线性表,顾名思义,是成线性的表,但是需要注意的是,这个成线性是体现在逻辑结构上的,物理结构上并不一定是连续的。我们常用的线性表有:数组、链表、字符串等等。
二、顺序表
1.概念:顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构。通常使用数组存储。
2.顺序表的分类:
2.1静态顺序表:使用定长数组存储元素。
例:
#define N 7
typedfef int SLDataType
typedef struct SeqList
{
SLDataType x[N];//定长数组
size_t size; //数组中的元素个数
}SL;
2.2动态顺序表:使用动态开辟的内存存储数据元素
例:
#define capacity 4
typedef int SLDataType
typedef struct SeqList
{
SLDataType *a; //动态开辟内存指针
size_t size; //数组中的有效个数
size_t capacity; //数组存储元素的最大限度(判断是否需要增容)
}SL;
3.顺序表的特点:
3.1静态顺序表:
①使用静态顺序表存储数据,必须要提前确定好顺序表的大小,也就是说使用之前就要知道需要存储的数据元素的个数。
②若开辟的空间刚好只能够存储需要存储的数据元素,那么在日后增加新的数据元素时会导致空间不够无法增加。
③存储单元的物理空间连续,可以随机访问,按位查找数据元素的时间复杂度为O(1),按值查找数据元素的时间复杂度为O(N)。
④增加和删除数据元素都比较麻烦,除非需要操作的数据元素在数组末尾,否则都需要移动数组中的元素,时间复杂度为O(N)。
3.2动态顺序表
①使用动态顺序表可以不必提前固定数组大小,申请适量的空间后根据情况判断是否需要开辟新的空间。
②存储单元的物理空间连续,可以随机访问,按位查找数据元素的时间复杂度为O(1),按值查找数据元素的时间复杂度为O(N)。
③增加和删除数据元素都比较麻烦,除非需要操作的数据元素在数组末尾,否则都需要移动数组中的元素,时间复杂度为O(N)。
4.顺序表的总结:
通过上述描述,大家应该对顺序表有了更深层次的认识,一起来看看顺序表的优缺点吧。
4.1 优点:
①不管是静态顺序表还是动态顺序表,都可以随机访问,按位查找的时间复杂度为O(1),按值查找的时间复杂度为O(N)。
②增加和删除的时间复杂度为O(N)。
4.2 缺点:
我想有个缺点大家应该很清楚,那就是开辟的空间大小:
①静态顺序表开辟好后,无法根据表中数据元素的个数变化来修改顺序表的大小,动态顺序表虽然做了优化 – 可以做到完整存储数据,可是也面临一个问题:若需要增加容量,增加好容量后只需增加一个数据元素,那么这依然对空间造成了浪费。
②顺序表删除数据元素,除非数据元素在末尾,否则就会牵一发而动全身。
基于顺序表上述的特点,为了更好的利用空间来存储数据元素,我们引荐另一位“大哥” – 链表!
三、链表
1.概念:链表就是物理存储结构未必连续,数据元素通过指针节点来实现逻辑连续的顺序表。
若我们创建一个存储四个元素的链表,效果图如下:
实际在内存中:
为了方便我们理解,我们可以加入线条来体现各个数据元素之间的线性关系:
2.种类:
我们最常见和最常用的就是 单向非循环链表 和 双向循环链表
3.链表的特点:
①可以更合理的利用好内存:因为不用开辟连续的空间,需要增加新的元素直接申请空间(吃多少拿多少),删除元素后直接释放空间。
②不可以随机访问,因为链表的物理存储空间不一定连续,所以不可以实现随机访问,而且单项非循环链表进行访问的话必须要从头结点开始,所以双向循环链表就显的更加方便。
③插入更加简单,不需要挪动链表其他数据元素,直接创建一个变量,改变其插入位置前后元素的链接属性即可,删除同理。
4.经典链表题:
4.1
链接:https://leetcode.cn/problems/linked-list-cycle/submissions/388920120/
代码如下:
bool hasCycle(struct ListNode *head) {
struct ListNode *fast,*slow;
fast = slow = head;
if((fast == NULL)||(fast->next == NULL))
{
return false;
}
fast = head->next->next;
slow = head->next;
while(slow != fast)
{
if((fast == NULL)||(fast->next == NULL))
{
return false;
}
fast = fast->next->next;
slow = slow->next;
}
return true;
}
思路:主要考的的是思路
我们先来简画一个带环链表分析:
像不像操场!?
①看到环,我们第一反应就应该想到两个指针,因为一个指针是无法对此链表是否为环进行判断的。
②这时我们就需要快慢指针了,就像两个人一起跑步,若设定A的速度恒快于B,A能与B相遇,那么,跑道肯定为环。换成指针同理。
③模拟场景:
④分析:我们分为两部分分析 – slow进环前,slow进环后
进环前:一开始从同一位置出发,fast速度是slow的两倍(最好slow一步,fast两步,后面解释),随着时间的推移,fast一定比slow先进环,这时我们要等的就是slow进环,而且slow一定会进环。
进环后:slow进环后,我们假定slow与fast之间的差距为N,这个N一定<环的大小,紧接着,slow -> 1,fast -> 2,我们可以理解为fast在追赶slow,因此slow与fast之间的距离减少了1,变为N-1,随着时间的推移,距离每次减少1,因此,fast迟早会追上slow,所以fast == slow就是证明是否为环的依据。
若此链表没有环:那就更好办了,链表没环,就一定会指向NULL,fast始终在slow前面,因此用fast来判断,fast == NULL或者fast->next == NULL,此链表无环。
补充:为何说fast最好走两步,slow走一步?
我们首先考虑的是有环的情况,经过上述推理我们得知,fast和slow进入环后,距离是在不断缩小的,而每次缩小的距离就是两个指针相差的步数,若缩小的步数不为1,那么就有可能造成fast超越slow,两个指针过后是否能 == 也是未知的,因此我们要保证在环内fast只是比slow快一步,至于为何fast两步、slow一步,考虑到链表本身的长度,若链表不为环,那么fast前进过大很有可能会越界,fast前进两步则只需要判断前两个元素的情况,综上所述,fast和slow的前进步数如此设置。
4.2
链接:https://leetcode.cn/problems/linked-list-cycle-ii/description/
代码如下:
struct ListNode *detectCycle(struct ListNode *head) {
struct ListNode *fast,*slow;
fast = slow = head;
if((fast == NULL)||(fast->next == NULL))
{
return NULL;
}
fast = head->next->next;
slow = head->next;
while(slow != fast)
{
if((fast == NULL)||(fast->next == NULL))
{
return NULL;
}
fast = fast->next->next;
slow = slow->next;
}
fast = head;
while(fast != slow)
{
fast = fast->next;
slow = slow->next;
}
return fast;
}
思路:
①判断是否为环可以复制粘贴上一题的代码(CV工程师)
②如何找到进入环的第一个节点呢? – 画图:
分析:我们可以知道的是,fast的速度是slow的
分析:我们可以知道的是,fast的速度是slow的2倍,也就是说fast的路程是slow的2倍,因此,我们可以得到一个等式 2*(L+C) = L+KR+C(环的大小未知,也许在slow进环之前,fast已经在环内移动若干圈了,用K表示),经过简化可得:
L+C = KR
L = KR-C
关键的一步来了!
KR-C是不是意味着,走了K圈然后减去距离C?那是不是可以理解成走了K-1圈,再加上R-C!
所以:L = (K-1)*R+R-C (不管R前面的系数是多少,都是整倍数)
所以,我们让其中一个节点在两个节点相遇的位置保留,另一个到头节点,然后同时前进1,再次相遇时就是进入环的第一个节点!
四、顺序表与链表的对比:
顺序表 | 链表 | |
插入元素 | 可能会需要移动大量元素:O(N) | 只需要在插入位置改变两侧元素的指针 |
删除元素 | 可能会需要移动大量元素:O(N) | 只需要改变删除元素前后元素的指针 |
增容 | 不够时要增加,很可能造成空间的浪费 | 需要时就开辟,不需要时就销毁,不会浪费空间 |
查找 | 支持随机访问 | 不支持随机访问 |
空间利用 | 必须开辟连续空间导致会浪费空间 | 有一个元素大小的空间就可以增加 |