上一篇文章《顺序表与链表的对比(一)》中给小伙伴们分享了顺序表与链表的初始化及定义,那么这篇将详细的给小伙伴们分享关于它们增(插入)的一些知识。
顺序表与链表的增
增可以理解为往里面添加数据,一般也叫插入数据。插入的方式分几种,可以选择在头部插入(简称 ”头插“ ),也可以在尾部插入(简称 “尾插” ),中间插入也可以,适用于不同的应用场景。
顺序表插入应用场景
顺序表使用连续的内存空间来存储数据,这使得数据的访问非常快速,因为可以通过索引直接访问任何元素。
优点:
-
快速访问:通过索引可以直接访问任何元素,时间复杂度为O(1)。
-
内存局部性:由于数据存储在连续的内存空间中,CPU缓存可以更有效地工作,提高访问速度。
缺点:
-
固定大小:数组的大小在创建时确定,扩容需要重新分配内存并复制数据,成本较高。
-
插入和删除成本高:在数组中间插入或删除元素需要移动大量元素,时间复杂度为O(n)。
应用场景:
-
数据访问频繁:当需要频繁访问数据时,数组是更好的选择,因为它提供了快速的随机访问能力。
-
数据量固定:当数据量不会频繁变化时,使用数组可以避免动态扩容的开销。
-
简单实现:对于简单的应用,数组的实现更直观和简单。
链表插入应用场景
链表通过节点之间的链接来存储数据,每个节点包含数据和指向下一个节点的指针。
优点:
-
动态大小:链表的大小可以动态变化,不需要预先分配固定大小的内存。
-
插入和删除效率高:在已知位置的节点前后插入或删除节点只需要改变指针,时间复杂度为O(1)。
缺点:
-
访问速度慢:访问链表中的元素需要从头节点开始遍历,时间复杂度为O(n)。
-
内存开销:每个节点需要额外的内存来存储指针。
应用场景:
-
频繁插入和删除:当需要频繁在数据结构中插入或删除元素时,链表是更好的选择,因为它可以高效地处理这些操作。
-
数据量变化大:当数据量可能会有较大变化时,链表可以动态调整大小,避免频繁扩容的开销。
-
内存碎片:在内存碎片较多的环境中,链表可以更好地利用内存。
选择顺序表还是链表,主要取决于应用的具体需求:
-
如果应用需要频繁访问元素,且数据量相对固定,顺序表是更好的选择。
-
如果应用需要频繁插入和删除元素,且数据量可能会有较大变化,链表是更好的选择。
在实际应用中,还可以根据具体情况选择其他数据结构,如栈、队列、哈希表等,以满足特定的需求
顺序表进行头插或者中间插入时,需保证数组空间充足,因为将首位置腾出空来时,需要将以前的数组元素往后挪位置,时间复杂度位O(n),若数组空间不够,容易引起数组越界问题。如果需要进行频繁的头插时,可以考虑使用链表结构。不过顺序表的尾插还是方便的,时间复杂度位O(1)但也要注意数组空间是否充足。
链表进行头插时,效率比顺序表要高,因为它不需要挪动数据,只需改变指针指向即可。但是进行中间插入时,如果不知道插入位置,则需要遍历链表,时间复杂度位O(n),也挺麻烦。先看代码吧~
1.顺序表的头插
// 头插法插入元素
void headInsert(SL *list, int x)
{
if (list->n == list->capacity) {
// 如果空间满了,扩容
list->capacity *= 2;
list->arr = (int*)realloc(list->arr, list->capacity * sizeof(int));
}
// 元素后移
for (int i = list->n; i > 0; i--) {
list->arr[i] = list->arr[i - 1];
}
// 插入新元素
list->arr[0] = x;
// 更新长度
list->n++;
}
2.1 单链表的头插
// 头插法插入
void insertAtHead(Node** head, int data)
{
Node* newNode = createNode(data);
newNode->next = *head;
//注意赋值先后顺序!!!
*head = newNode;
}
2.2 双链表的头插
// 头插法插入
void insertAtHeadDoubly(Node** head, int data)
{
Node* newNode = createNode(data);
newNode->next = *head;
if (*head != NULL)
{
(*head)->prev = newNode;
}
*head = newNode;
}
3.顺序表的尾插
顺序表的尾插其实跟头插差不多。只不过不需要挪动数据,注意好数组空间是否充足就行。
// 尾插法插入元素
void insertAtTail(SeqList *list, int data)
{
if (list->n == list->capacity)
{
// 如果空间满了,扩容
list->capacity *= 2;
list->arr = (int*)realloc(list->arr, list->capacity * sizeof(int));
}
list->arr[list->n] = data;
list->n++;
}
4.1单链表的尾插
链表进行尾插时,需要检查链表是否为空,防止出现空指针问题。
// 尾插法插入
void insertAtTail(Node** head, int data)
{
Node* newNode = createNode(data);
if (*head == NULL)
{
*head = newNode;
return;
}
Node* temp = *head;
while (temp->next != NULL)
{
temp = temp->next; //找尾结点
}
temp->next = newNode;
}
4.2 双链表的尾插
与单链表一样,需检查链表是否为空,不过如果带有哨兵位的头节点时,可以省略。
// 尾插法插入
void insertAtTailDoubly(Node** head, int data)
{
Node* newNode = createNode(data);
if (*head == NULL)
{
*head = newNode;
return;
}
Node* temp = *head;
while (temp->next != NULL)
{
temp = temp->next;
}
temp->next = newNode;
newNode->prev = temp;
}
5.1单链表的随机插入
如果知道需要插入的位置时,就不用遍历,另外这里可以再单独写一个函数用来查找位置,通过其返回的指针,来进行插入(也可分前插和后插),后续删除也会使用到。
// 指定位置插入
void insertAtPosition(Node** head, int data, int position)
{
if (position == 0)
{
insertAtHead(head, data);
return;
}
Node* newNode = createNode(data);
Node* temp = *head;
for (int i = 0; i < position - 1 && temp != NULL; i++)
{
temp = temp->next;
}
if (temp == NULL)
{
free(newNode);
return; // 位置无效
}
newNode->next = temp->next;
temp->next = newNode;
}
5.2双链表的随机插入
// 指定位置插入
void insertAtPositionDoubly(Node** head, int data, int position)
{
if (position == 0)
{
insertAtHeadDoubly(head, data);
return;
}
Node* newNode = createNode(data);
Node* temp = *head;
for (int i = 0; i < position - 1 && temp != NULL; i++)
{
temp = temp->next;
}
if (temp == NULL)
{
free(newNode);
return; // 位置无效
}
newNode->next = temp->next;
if (temp->next != NULL)
{
temp->next->prev = newNode;
}
temp->next = newNode;
newNode->prev = temp;
}
希望你开心吖@