通过链式存储方式的线性表,每一个结点包含了数据元素以及指向后继结点的指针
算法中考虑方面:
- 如何判断是否为空
- 如何判断表头还是表尾
- 表头、表中、表尾的插入与删除
单链表主要有两种(都必须掌握):
- 带头结点,在链头之前加入一个空结点,好处是在链头这个位置不需要进行特殊判断
- 不带头结点
单链表特点:
- 不要求大量连续区域
- 可以实现动态增加动态减少
- 不可以随机存取
- 并且需要额外开销存放指针
时间复杂度:
- 进行按位序插入,时间复杂度为O(n)
- 按位序删除,时间复杂度O(n)
- 进行给定结点的前插后插操作,时间复杂度为O(1)
- 进行给定结点的删除操作,时间复杂度为O(1)
带头节点
带头结点所有相关操作
//声明节点结构体
typedef struct LNode{
ElemType data;
LNode *next;
}LNode, *LinkList;
//typedef说明跳转至专栏中函数说明博客
//按位序插入
bool InsertList(LinkList &L, int i, Elem e){
//寻找第i-1个结点的前驱结点
if (i < 1) //插入越界判断
return false;
int j = 0;
LNode *p = L;
while (p != NULL && j < i - 1){
p = p->next;
j ++;
}
if (p == NULL) //说明输入i的值超越范围
return false;
//完成后插操作
LNode *new_node = (LNode *) malloc (sizeof(LNode)); //产生新结点插入后方
new_node->data = e;
new_node->next = p->next;
p->next = new_node;
return true;
}
//完成指定结点后插操作
bool InsertList(LNode * node, ElemType e){
if (node == NULL) //判断输入是否有误
return false;
LNode * p = (LNode *)malloc(sizeof(LNode)); //完成后插操作
if (p == NULL)
return false;
p->data = e;
p->next = node->next;
node->next = p;
return true;
}
//完成指定结点的前插操作,偷天换日
bool InsertPrior(LNode *node, ElemType e){
if (node == NULL)
return false;
//无法知道当前结点的前一个结点,但是知道当前结点的后一个
//可以将当前结点变为要插入的结点,生成一个备份后一个结点
LNode * p = (LNode *)malloc(sizeof(LNode));
p->next = node;
p->data = node->data;
node->data = e;
return true;
}
//删除后继结点
//存在小问题,如果删除的是最后一个结点,将无法删除,因为无法获得前驱结点
bool LNodeDelete(LNode * node, ElemType &e){
if (node == NULL)
return false;
if (node->next == NULL)
return false;
LNode * p = node->next;
e = p->data;
node->next = p->next;
free(p);
return true;
}
//按位序删除
bool DeleteByPos(LinkList &L, int i, ElemType &e){
if (i < 1)
return false;
int j = 0;
LNode *p = L;
while (p != NULL && j < i - 1){
p = p->next;
j ++;
}
if (p == NULL) //如果p为null说明输入i超过了length+1
return false;
if (p->next == NULL) //如果p不为null而p的next为null说明输入i恰巧为length+1
return false;
LNode *tem = p->next;
p->next = tem->next;
e = tem->data;
free(tem);
return true;
}
//按值删除
bool deleteByValue(LinkList &L, ElemType e, ElemType &e){
LNode * p = L;
while (p->next != NULL && p->next->data != e)
p = p->next;
if (p->next == NULL){
return false;
}
LNode * tem = p->next;
e = tem->data;
p->next = tem->next;
free(tem);
return true;
}
//按位序查找
LNode * searchList(LinkList L, int i){
if (i < 1 ){
return NULL;
}
int j = 0;
LNode * p = L;
while (p != NULL && j < i){
p = p->next;
j ++;
}
return p;
}
//判断链表是否为空
bool isEmpty(LinkList L){
return L->next == NULL;
}
//链表置空
void ClearLinkList(LinkList & L){
LNode * p = L->next;
L->next = NULL;
while (p != NULL){
LNode tem = p;
p = p->next;
free(tem);
}
}
//求表长
int Length(LinkList L){
LNode * p = L;
int i = 0;
while (p->next != NULL){
i++;
p = p->next;
}
return i;
}
链表的初始化插入有两种方式
- 头插法,即来一个数据元素,直接插入到头指针的下一个,链表存储数据与输入顺序相反
- 尾插法,来一个元素,插入到链尾的下一个,链表存储数据与输入顺序相同
//头插法插入
void InitLinkList(LinkList & L){
L = (LNode *)malloc(sizeof(LNode));
L->next = NULL;
LNode * p = L;
LNode * q;
ElemType value;
while (scanf(“%d”, &value) != EOF){
q = (LNode *)malloc(sizeof(LNode));
q->next = p->next;
q->data = value;
p->next = q;
}
}
//尾插法插入数据
void InitLinkList(LinkList &L){
L = (LNode *)malloc(sizeof(LNode));
L->next = NULL;
LNode * p = L;
LNode * q;
ElemType value;
while (scanf(“%d”, &value) != EOF){
q = (LNode *)malloc(sizeof(LNode));
q->next = p->next;
q->data = value;
p->next = q;
q = q->next;
}
}
//尾插法有一个具体应用是反转链表,当然这是数据的反转,通过双指针实现
void LinkListReverse(LinkList &L){
LNode * p = L;
LNode * q = L->next;
p->next = NULL;
while (q != NULL){
LNode * tem = q->next;
q->next = p->next;
p->next = q;
q = tem;
}
}
不带头节点
最大的特点是头结点需要特殊判断,因为一般来说,修改节点会指向他的直接前驱,那么非头结点就是修改前驱结点的next值,而头结点需要修改指针。
其余操作类似,将头指针单独拿出来进行判断。需要注意计数器也要进行修改。
//按位序插入,i为位序
bool insertLinkList (LinkList &L, int i, ElemType e){
if (i < 1)
return false;
if (i == 1){
LNode * p = (LNode *)malloc(sizeof(LNode));
p->next = L;
p->data = e;
L = p;
return true;
}
int j = 1;
LNode * p = L;
for (p != NULL && j < i){
p = p->next;
j ++;
}
if (p == NULL)
return false;
LNode * q = (LNode *)malloc(sizeof(LNode));
q->next = p->next;
q->data = e;
p->next = q;
return true;
}