顺序表缺点
顺序表随机访问很方便,但是也会有不足啊:
1、挪动数据时间开销较大:头部/中间的插入删除,需要挪动后面的所有数据,时间复杂度为O(N)
2、增容有代价:增容需要重新申请空间,拷贝数据,释放旧空间,系统消耗不小
3、空间浪费:增容一般增至原来的2倍大空间,会有空间浪费,假如当前容量为100,满了以后增容到200,再继续插入了5个数据,后面没有插入数据了,这就浪费了95个空间
不存在扩容代价、不存在空间浪费、按需申请空间、头部或者中间插入数据而不需要挪动数据的链表可以解决以上问题
链表的概念和结构
链表是一种物理存储结构上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的
注意:
1、链式结构在逻辑上连续,物理结构上不一定连续。(逻辑结构是想象的,物理结构是内存中实际存储的)
2、结点一般从堆上申请
3、堆上申请的空间时按照一定策略分配的,两次申请的空间可能连续也可能不连续
链表的分类
有8种链表结构
(1)单向和双向
(2)带头单链表和不带头单链表
(3)非循环链表和循环链表
以上3种分类的链表一共有222=8种组合,最实用的有两种:
1、无头单向非循环链表:结构简单,一般不会用来单独存储数据。十几种更多是作为其他数据结构的子结构,如哈希桶,图的邻接表等。
2、带头双向循环链表:结构最复杂,一般用来单独存储数据。实际中使用的链表数据结构都是带头双向循环链表。虽然结构复杂,但是使用代码实现以后会发现该结构会带来很多优势,实现反而简单
链表的实现
SList.h 链表函数定义
typedef int SLTDataType;
typedef struct SListNode
{
SLTDataType data;
struct SListNode* next;
}SLTNode;
//单向+不带头+非循环
//打印
void print(SLTNode* plist);
//尾插
void SListNodePushBack(SLTNode** plist,SLTDataType x);
//头插
void SListPushFront(SLTNode** plist, SLTDataType x);
//尾删
void SListPopBack(SLTNode** plist);
//头删
void SListPopFront(SLTNode** plist);
//单链表查找
SLTNode* SListFind(SLTNode* plist, SLTDataType x);
//在pos之后插入x
void SListInsertAfter(SLTNode* pos, SLTDataType x);
//在pos之前插入x
void SListInsertBefore(SLTNode** plist,SLTNode* pos, SLTDataType x);
//在pos之前插入x,没有头的情况:后插一个值为x的节点,再跟前面的结点值交换。
void SListInsertBeforeNoHead(SLTNode* pos, SLTDataType x);
//删除后一个节点
void SListEraseAfter(SLTNode* pos);
//删除当前位置
void SListEraseCur(SLTNode** plist, SLTNode* pos);
SList.c 链表函数实现
#include<stdio.h>
#include<stdlib.h>
#include<assert.h>
#include "SList.h"
//打印
void SListPrint(SLTNode* plist)
{
SLTNode* cur = plist;
//找空节点,找到后不再打印
while (cur != NULL)
{
printf("%d->", cur->data);
//让cur指向下一个节点
cur = cur->next;
}
printf("NULL\n");
}
//创建新节点
SLTNode* BuySLTNode(SLTDataType x)
{
//申请空间
SLTNode* node = (SLTNode*)malloc(sizeof(SLTNode));
if (node == NULL)
{
return NULL;
}
//数据赋值
node->data = x;
//指针赋值
node->next = NULL;
return node;
}
1、尾插:
void SListPushBack(SLTNode** pplist, SLTDataType x)
{
SLTNode* newNode = BuySLTNode(x);
//不能使用tail->next != NULL直接作为判断条件,因为有可能是空链表,对空指针解引用会造成非法访问,因此要分两种情况
if (*pplist == NULL)//链表为空
{
*pplist = newNode;
}
else
{
SLTNode* tail = *pplist;
while(tail->next!=NULL)
{
tail=tail->next;
}
tail->next=newNode;
}
}
2、头插,不需要区分空链表和非空链表
void SListPushFront(SLTNode** pplist, SLTDataType x)
{
SLTNode* newNode = BuySLTNode(x);
newNode->next=*pplist;
*pplist=newNode;
}
3、尾删
void SListPopBack(SLTNode** pplist)
{
if(*pplist==NULL)
{
return;
}
else if((*pplist->next)==NULL)
{
free(*pplist);
*pplist=NULL;
}
else
{
SLTNode* pre=NULL;
SLTNode* tail = *pplist;
while(tail->next!=NULL)
{
pre=tail;
tail=tail->next;
}
free(tail);
tail=NULL;
pre->next=NULL;
}
}
4、头删
void SListPopFront(SLTNode** pplist)
{
if(*pplist==NULL)
{
return;
}
else
{
SLTNode* new = (*pplist)->next;
free(*pplist);
*pplist=NULL;
}
}
5、单链表查找
SLTNode* SListFind(SLTNode* plist, SLTDataType x)
{
SLTNode *cur=*pplist;
while(cur)
{
if(cur->data==x)
{
return cur;
}
cur=cur->next;
}
return NULL;
}
6、在pos之后插入
void SListInsertAfter(SLTNode* pos, SLTDataType x)
{
SLTNode* newNode = BuySLTNode(x);//插入需要先创建结点
newNode->next=pos->next;
pos->next=newNode;
}
7、在pos之前插入x
void SListInsertBefore(SLTNode** pplist, SLTNode* pos, SLTDataType x)
{
SLTNode* newNode = BuySLTNode(x);
if(pos==*pplist)
{
newNode->next=pos;
*pplist=newNode;
}
else
{
SLTNode* prev = NULL;//用来记录pos的前一个结点
SLTNode* cur = *pplist;
while(cur!=pos)
{
prev=cur;
cur=cur->next;
}
newNode->next=cur;
prev->next=newNode;
}
}
8、在pos之前插入x,没有头(plist):后插一个值为x的节点,再跟前面的结点值交换
void SListInsertBeforeNoHead(SLTNode* pos, SLTDataType x)
{
SLTNode* newNode = BuySLTNode(x);
//后插新节点
newNode->next = pos->next;
pos->next = newNode;
//将新结点的值和pos的值进行交换
SLTDataType temp = pos->data;
newNode->data = temp;
pos->data = x;
}
9、删除后一个结点
//删除后一个节点
void SListEraseAfter(SLTNode* pos)
{
assert(pos);
if (pos->next == NULL)
{
return;
}
SLTNode* next = pos->next;
pos->next = next->next;//pos的下一个指向pos的下一个结点的下一个结点
free(next);
next = NULL;
}
10、删除当前位置
//删除当前位置
void SListEraseCur(SLTNode** pplist, SLTNode* pos)
{
assert(pos);
SLTNode* prev = NULL;
SLTNode* cur = *pplist;
while (cur != pos)//寻找pos
{
prev = cur;
cur = cur->next;
}
prev->next = cur->next;//前一个结点的下一个指向当前结点即pos的下一个
free(cur);
cur = NULL;
}
链表应用OJ题
1.删除链表中等于给定值val的所有节点
分析:
(1)要删除某一结点,需要保存该结点的前一个结点(删除当前节点后,前一结点应指向当前结点的下一结点),同时需要保存当前结点的下一结点(删除当前结点后,能够继续向后访问该链表)
(2)操作:
遍历链表,如果当前结点值==val,就保存当前结点的下一个结点,前一结点指向当前结点的下一结点,释放当前结点,当前结点指向下一结点;
如果当前结点值!=val,前一结点挪到当前结点位置,当前结点挪到下一结点位置。需要考虑特殊情况,第一个结点值 == val时,此时prev=NULL需要特殊处理。
不带哨兵位头结点解法:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode* removeElements(struct ListNode* head, int val){
struct ListNode* prev = NULL;
struct ListNode* cur = head;
while(cur)
{
if(cur->val == val)//找到了
{
struct ListNode* next = cur->next;//要free(cur),就必须要先保存cur的下一个结点,否则无法继续访问下一结点
if(prev == NULL)//cur是头,要删除的结点在第一个位置
{
free(cur);
head = next;
cur = next;
}
else
{
prev->next = next;
free(cur);
cur = next;
}
}
else
{
prev = cur;
cur = cur->next;
}
}
return head;
}
带哨兵位头结点解法:
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode* removeElements(struct ListNode* head, int val)
{
struct ListNode* guardHead = (struct ListNode*)malloc(sizeof(struct ListNode));
guardHead->next = head;
struct ListNode* prev = guardHead;
struct ListNode* cur = head;
while (cur)
{
if (cur->val == val)
{
struct ListNode* next = cur->next;
prev->next = next;
free(cur);
cur = next;
}
else
{
prev = cur;
cur = cur->next;
}
}
head = guardHead->next;
free(guardHead);
guardHead = NULL;
return head;
}
2.反转一个单链表
分析:
解法一:如果只定义两个节点,当n2指向n1时,n2的下一个结点就找不到了。因此要定义3个节点,n1和n2用于翻转,n3用于迭代:n1指向NULL,n2指向n1,n3用于记录n2的下一个结点
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode* reverseList(struct ListNode* head)
{
if(head == NULL || head->next == NULL)
{
return head;
}
struct ListNode* n1 = NULL,*n2 = head,*n3 = head->next;
while(n2)
{
n2->next = n1;
n1 = n2;
n2 = n3;
if(n3)
{
n3 = n3->next;
}
}
return n1;
}
解法二:头插法,依次取原链表中的节点头插到新节点
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode* reverseList(struct ListNode* head)
{
struct ListNode* cur = head;
struct ListNode* newHead = NULL;
while(cur)
{
struct ListNode* next = cur->next;
cur->next = newHead;
newHead = cur;
cur = next;
}
return newHead;
}
3.链表的中间结点
分析:只能遍历一遍,可以用快慢指针,慢指针每次走一步,快指针每次走两步
/**
* Definition for singly-linked list.
* struct ListNode {
* int val;
* struct ListNode *next;
* };
*/
struct ListNode* middleNode(struct ListNode* head)
{
struct ListNode* slow = head;
struct ListNode* fast = head;
while(fast && fast->next)
{
slow = slow->next;
fast = fast->next->next;
}
return slow;
}
4.输出链表倒数第k个结点
struct ListNode
{
int val;
struct ListNode *next;
};
struct ListNode* FindKthToTail(struct ListNode* pListHead, int k )
{
if(pListHead == NULL)
{
return NULL;
}
struct ListNode* slow = pListHead;
struct ListNode* fast = pListHead;
while(k--)
{
if(fast == NULL)
{
return NULL;
}
fast = fast->next;
}
while(fast)
{
slow = slow->next;
fast = fast->next;
}
return slow;
}
5.合并两个有序链表
struct ListNode {
int val;
struct ListNode *next;
};
struct ListNode* mergeTwoLists(struct ListNode* l1, struct ListNode* l2)
{
struct ListNode* head,*tail = NULL;
if(l1 == NULL)
{
return l2;
}
if(l2 == NULL)
{
return l1;
}
if(l1->val < l2->val)
{
head = tail = l1;
l1 = l1->next;
}
else
{
head = tail = l2;
l2 = l2->next;
}
while(l1 && l2)
{
if(l1->val < l2->val)
{
tail->next = l1;
l1 = l1->next;
tail = tail->next;
}
else
{
tail->next = l2;
l2 = l2->next;
tail = tail->next;
}
}
if(l1 == NULL)
{
tail->next = l2;
}
else
{
tail->next = l1;
}
return head;
}