在讨论链表(linked-list)之前,需要明确几个概念: 线性表(顺序表, list, linear list),
数组(array),链表(linked-list)。
线性表:在中文里,线性表也叫做顺序表。在英文中,它称为list, linear list等。它是最基
础、最简单、最常用的一种基本数据结构,线性表存储的每个数据称为一个元素,各个元素及其
索引是一一对应的关系。线性表有两种存储方式:顺序存储方式和链式存储方式。
数组(array):数组就是线性表的顺序存储方式。数组的内存是连续分配的,并且是静态分配的,
即在使用数组之前需要分配固定大小的空间。可以通过索引直接得到数组中的元素,即获取数组
中元素的时间复杂度为O(1)。
链表(linked-list):链表就是线性表的链式存储方式。链表的内存是不联系的,前一个元素的物理
地址的下一个地址中存储的不一定是下一个元素。链表通过一个指向下一个元素地址的引用(或指针)
将链表中的元素串起来。
其实为了简便,通常我们直接将list看作是链表。但是也不必太过纠结这种名称定义,更重要的还是
结构的实现。
链表分类
链表分为单向链表(Singly linked list)、双向链表(Doubly linked list)、 循环链表(Circular
Linked list)。
单向链表(Singly linked list)
单向链表是最简单的链表形式。我们将链表中最基本的数据称为节点(node), 每一个节点都包含
了数据块和指向下一个节点的指针。
单向链表有时候也分为有头结点和无头结点。有头结点的链表实现比较方便(每次插入新元素的时候,
不需要每次判断第一个节点是否为空),并且可以直接在头结点的数据块部分存储链表的长度,
而不用每次都遍历整个链表。
在链表中插入一个新的元素有两种方式:后插和前插。后插就是每次在链表的末尾插入新元素,
前插就是在链表的头插入新元素。
后插法比较符合平常的思维方式,并且保证插入数据的先后顺序。但是由于只保存了头结点,
所以每次插入新元素必须重新遍历到链表末尾。为了解决这个问题,考虑增加一个尾指针,
指向链表的最后一个节点。
元素顺序与插入顺序相反。
由于单向链表只存储了头指针,所以删除单向链表中的元素时,需要找到目标节点的前驱节点。
由于链表里面的内存是手动分配的,当不再使用这些内存时需要手动删除。
顾名思义,双向链表就是有两个方向的链表。同单向链表不同,在双向链表中每个节点不仅存储指向下一个
节点的指针,而且存储指向前一个节点的指针。通过这种方式,能够通过在O(1)时间内通过目的节点直接找
到前驱节点,但是同时会增加大量的指针存储空间。
在双向链表中插入新元素的操作跟在单向链表中插入新元素的操作类似。
由于双向链表中每个节点记录了它的前驱节点,所以不需要像单向链表中一样索引目的节点的前驱节点,
而是可以通过目标节点直接获取。
如何判断当前节点是否为第一个节点?
如何判断当前节点是否为最后一个节点?
在双向链表中,第一个节点的前驱节点不是头结点,而是指向一个空指针。同样的,最后一个节点的后驱指向
了一个空指针。
循环链表(Circular Linked list)
循环链表与双向链表相似,不同的地方在于:在链表的尾部增加一个指向头结点的指针,头结点也增加一个指
向尾结点的指针,以及第一个节点指向头节点的指针,从而更方便索引链表元素。
插入、删除
循环链表的插入和删除操作与双向链表的实现方式一样。
判断空链表、链表头和尾
数组(array),链表(linked-list)。
线性表:在中文里,线性表也叫做顺序表。在英文中,它称为list, linear list等。它是最基
础、最简单、最常用的一种基本数据结构,线性表存储的每个数据称为一个元素,各个元素及其
索引是一一对应的关系。线性表有两种存储方式:顺序存储方式和链式存储方式。
数组(array):数组就是线性表的顺序存储方式。数组的内存是连续分配的,并且是静态分配的,
即在使用数组之前需要分配固定大小的空间。可以通过索引直接得到数组中的元素,即获取数组
中元素的时间复杂度为O(1)。
链表(linked-list):链表就是线性表的链式存储方式。链表的内存是不联系的,前一个元素的物理
地址的下一个地址中存储的不一定是下一个元素。链表通过一个指向下一个元素地址的引用(或指针)
将链表中的元素串起来。
其实为了简便,通常我们直接将list看作是链表。但是也不必太过纠结这种名称定义,更重要的还是
结构的实现。
链表分类
链表分为单向链表(Singly linked list)、双向链表(Doubly linked list)、 循环链表(Circular
Linked list)。
单向链表(Singly linked list)
单向链表是最简单的链表形式。我们将链表中最基本的数据称为节点(node), 每一个节点都包含
了数据块和指向下一个节点的指针。
typedef struct node
{
int val;
struct node *next;
}Node;
头结点
单向链表有时候也分为有头结点和无头结点。有头结点的链表实现比较方便(每次插入新元素的时候,
不需要每次判断第一个节点是否为空),并且可以直接在头结点的数据块部分存储链表的长度,
而不用每次都遍历整个链表。
//create a new node with a value
Node* CreateNode(int val)
{
Node *newNode = (Node*)malloc(sizeof(Node));
if(newNode == NULL)
{
printf("out of memory\n");
return NULL;
}
else
{
newNode->val = val;
newNode->next = NULL;
return newNode;
}
}
int main()
{
Node *head = CreateNode(0);
//insert new value into list, end with END_INPUT(999)
int value;
while(scanf("%d", &value) && value != END_INPUT)
{
Insert(head, value);
}
return 0;
}
插入
在链表中插入一个新的元素有两种方式:后插和前插。后插就是每次在链表的末尾插入新元素,
前插就是在链表的头插入新元素。
后插法比较符合平常的思维方式,并且保证插入数据的先后顺序。但是由于只保存了头结点,
所以每次插入新元素必须重新遍历到链表末尾。为了解决这个问题,考虑增加一个尾指针,
指向链表的最后一个节点。
void Insert(Node *head, Node *tail, int val)
{
Node *newNode = CreateNode(val);
tail->next = newNode;
tail = tail->next;
head->val++;
}
由于前插法是在头部插入新元素,那么每次增加新元素可以直接通过头指针索引,但是得到的
元素顺序与插入顺序相反。
void Insert(Node *head, int val)
{
Node *newNode = CreateNode(val);
newNode->next = head->next;
head->next = newNode;
head->val++;
}
删除
由于单向链表只存储了头指针,所以删除单向链表中的元素时,需要找到目标节点的前驱节点。
void DeleteByVal(Node *head, int val)
{
if(head->next == NULL)
{
printf("empty list\n");
return;
}
//find target node and its precursor
Node *cur = head->next, *pre = head;
while(cur)
{
if(cur->val == val)
break;
else
{
cur = cur->next;
pre = pre->next;
}
}
//delete target node
if(cur != NULL)
{
pre->next = cur->next;
free(cur);
head->val--;
}
}
清空链表
由于链表里面的内存是手动分配的,当不再使用这些内存时需要手动删除。
void Free(Node *head)
{
for(Node *temp = head; temp != NULL; temp = head)
{
head = head->next;
free(temp);
}
}
链表反转
Node* Reverse(Node* head)
{
if(head == NULL || head->next == NULL)
return head;
else{
Node *cur = head->next,
*pre = NULL,
*next = NULL;
while(cur != NULL)
{
next = cur->next;
cur->next = pre;
pre = cur;
cur = next;
}
head->next = pre;
return head;
}
}
双向链表(Doubly linked list)
顾名思义,双向链表就是有两个方向的链表。同单向链表不同,在双向链表中每个节点不仅存储指向下一个
节点的指针,而且存储指向前一个节点的指针。通过这种方式,能够通过在O(1)时间内通过目的节点直接找
到前驱节点,但是同时会增加大量的指针存储空间。
typedef struct nodeDly
{
int val;
struct nodeDly *pre;
struct nodeDly *next;
}NodeDly;
插入
在双向链表中插入新元素的操作跟在单向链表中插入新元素的操作类似。
NodeDly* CreateNode(int val)
{
NodeDly *newNode = (NodeDly*)malloc(sizeof(NodeDly));
if(newNode == NULL)
{
printf("out of memory!\n");
return NULL;
}
else
{
newNode->val = val;
newNode->pre = NULL;
newNode->next = NULL;
return newNode;
}
}
void Insert(NodeDly *head, int val)
{
NodeDly *newNode = CreateNode(val);
newNode->next = head->next;
head->next->pre = newNode;
head->next = newNode;
}
删除
由于双向链表中每个节点记录了它的前驱节点,所以不需要像单向链表中一样索引目的节点的前驱节点,
而是可以通过目标节点直接获取。
NodeDly* FindByVal(NodeDly *head, int val)
{
for(NodeDly* temp = head; temp != NULL; temp = temp->next)
{
if(temp->val == val)
return temp;
}
return NULL;
}
void DeleteByVal(NodeDly head, int val)
{
NodeDly *target = FindByVal(head, val);
if(target == NULL)
{
printf("not find target value!\n");
return;
}
target->pre->next = target->next;
target->next->pre = target->pre;
free(target);
}
其他
如何判断当前节点是否为第一个节点?
如何判断当前节点是否为最后一个节点?
在双向链表中,第一个节点的前驱节点不是头结点,而是指向一个空指针。同样的,最后一个节点的后驱指向
了一个空指针。
循环链表(Circular Linked list)
循环链表与双向链表相似,不同的地方在于:在链表的尾部增加一个指向头结点的指针,头结点也增加一个指
向尾结点的指针,以及第一个节点指向头节点的指针,从而更方便索引链表元素。
插入、删除
循环链表的插入和删除操作与双向链表的实现方式一样。
判断空链表、链表头和尾