前言
数据结构,一门数据处理的艺术,精巧的结构在一个又一个算法下发挥着他们无与伦比的高效和精密之美,在为信息技术打下坚实地基的同时,也令无数开发者和探索者为之着迷。
也因如此,它作为博主大二上学期最重要的必修课出现了。由于大家对于上学期C++系列博文的支持,我打算将这门课的笔记也写作系列博文,既用于整理、消化,也用于同各位交流、展示数据结构的美。
此系列文章,将会分成两条主线,一条“数据结构基础”,一条“数据结构拓展”。“数据结构基础”主要以记录课上内容为主,“拓展”则是以课上内容为基础的更加高深的数据结构或相关应用知识。
欢迎关注博主,一起交流、学习、进步,往期的文章将会放在文末。
这一节我们将开始总结一个常见的数据结构:线性表。
由于线性表的内容较多,本节只能总结部分内容。
主要为两种基础的线性表,数组和链表。在简单介绍其定义之后,重点会放在实现对其各种操作的简单实现及封装。
本节思维导图如下:
链式存储结构——链表
链表也是一种线性表,相较于数组不同的是,链表中的元素的存储结构是链接式的。
作为一种线性表,元素在逻辑上是连续的,有顺序且呈线性排列;但在存储上他们并不像顺序表一般占据连续的空间,而是随机分布在空间各处,每个结点通过指针来记录后继的地址形成链接。
从上面例子中也可看出,每个结点在其所占的空间中除了要存储本身的数据外,还需要单独使用一片空间存储其后继的指针。
所以一个结点可以大致表示成如下形式:
他们链接起来可以表示成:
可以注意到,链表的尾结点没有后继,所以其后继指针需要指向空。且由于链表的内容是随机分布在空间中的各处。所以为了便于访问他们,我们需要保留头结点的指针,也就是知道头结点的位置,对于后面的元素便可以通过后继节点依次访问到他们。
链表的优点在于其空间上的灵活,链接式的存储似的其长度可以任意改变,不像数组的长度是定值。缺点就在于其失去了顺序表中静态访问结点的能力,不能直接按照下标访问某元素,使得访问给定下标的元素必须遍历链表。
单链表的实现
知道了链表的设计方案,下面我们就来将设计方案付诸实践,变成代码。
实际上正如上文所说,链表的内容相较于一般的节点来说只是多出了后继指针,所以我们完全可以在一般的数据结点的基础上加上后继指针来得到链表元素。
单链表元素
//C
typedef struct _Node{
int value;
struct _Node * next;
}Node;
Node * head;
//Java
class Node{
int value;
Node next;
}
Node head;
单链表的遍历
遍历整个单链表,必不可少的条件就是获得头指针。有了头指针就可以根据后继指针依次访问后面的元素。
那么什么时候链表遍历结束呢?通过上文的例子不难发现,链表的尾结点的后继指针为空,所以可以根据是否为空来判断这一结点是否是有效结点,这样既可以在遍历过尾结点后准确而迅速的结束遍历,也可以应对链表为空的情况(头指针为空)
下面以遍历链表获得所有元素加和为例:
//C
int getSum(Node * head){
int sum = 0;
while(head){
sum += head->value;
head = head->next;
}
return sum;
}
//java
class LinkedList{
Node head;
public int getSum(){
int sum = 0;
Node cur = head;//不能直接使用头引用进行遍历
while(cur != null){
sum += cur.value;
cur = cur.next;
}
return sum;
}
}
复杂度:O(n)
插入元素
向链表的优秀之处在于其数据组织是动态的,插入一个元素可以不需要像数组一样将后面的元素向后移动。可以通过对后继指针的操作来完成插入。
以将元素node
插入下标为index(0 ≤ index ≤ len)
的位置为例,该函数需要返回操作后的头指针:
//C
Node * insert(Node * head,Node * node,int index){
int curIdx = 0;
if(index == 0){
//新元素位于队首
node->next = head;
return node;
}
while(head){
if(curIdx = index - 1 || head->next = NULL){
node->next = head->next;
head->next = node;
break;
}
head