废话不多说,直接上干货
这里我们比较头节点不存储数据和头节点存储数据两种情况下的结构特点
1.链表的结构定义
//头节点没有数据
typedef struct Node{
int data;//存放数据
Node* next;//指向下一个节点
Node(): data(0),next(NULL){} //头节点直接调用默认构造就可以了
Node(int x): data(x),next(NULL){}
}*ListNode;
//头节点存储数据
typedef struct Node1{
int data;//存放数据
Node1* next;//指向下一个节点
Node1(int x): data(x),next(NULL){}
}*ListNode1;
int main()
{
ListNode head = new Node();
cout << head->data << endl;
ListNode1 head1 = new Node1(10);
cout << head1->data << endl;
system("pause"); // 防止运行后自动退出,需头文件stdlib.h
return 0;
}
上述定义了一个单链表的节点,包含数据和后继指针,使用了两种方法,一种是头节点有数据,一种是头节点无数据。
- 这里采用初始化列表的方式初始化Node,不仅仅是因为这样书写简洁方便,而是这种方式在有些时候是比在函数体里面初始化更加的高效,希望能养成习惯。
- 使用typedef的方式在下方定义的*ListNode是一个结构体类型而不是一个变量。
2.建立一个线性链表
这里是将数组元素生成为一个单链表,复杂度为O(n)。
先采用头节点无数据的实现
template<typename T>
void CreateList(ListNode &link_list, T arr, int len){
Node* p = link_list;
for(int i=0; i<len; i++){
p->next = new Node(arr[i]);
p = p->next;
}
}
void ShowList(const ListNode &link_list){
Node* p = link_list->next;
if(NULL == p) return;
while(NULL != p->next){
cout << p->data << "\t";
p = p->next;
}
cout<<p->data << endl;
}
int main()
{
vector<int> a = {1,2,3,4,5};
int b[5] = {1,2,3,4,5};
ListNode link_list = new Node();
CreateList(link_list, a, a.size());
ShowList(link_list);
system("pause"); // 防止运行后自动退出,需头文件stdlib.h
return 0;
}
对于链表的操作,常常需要定义一个额外的指针,用于指向链表这个数据结构,并进行数据的访问。
- 这里
CreateList
采用了泛型,但是这里不想麻烦了,自己懂就可以了。 - 这里因为头节点不存储数据,所以在输出时,就进行了一个判断。
下面是头节点存储数据的实现
template<typename T>
void CreateList(ListNode link_list, T arr, int len){
if(len == 0) return;
link_list = new Node(arr[0]);
Node* p = link_list;
for(int i=1; i<len; i++){
p->next = new Node(arr[i]);
p = p->next;
}
}
void ShowList(const ListNode &link_list){
Node* p = link_list;
if(NULL == p) return;
while(NULL != p->next){
cout << p->data << "\t";
p = p->next;
}
cout<<p->data << endl;
}
头节点有数据,在创建的时候,需要注意给link_list表创建头节点
3.求线性链表的长度
同样的,先是头节点无数据的
int LinkListLen(const ListNode &link_list){
Node* p = link_list->next;
if(NULL == p) return 0;
int len = 1;
while(NULL != p->next){
p = p->next;
len++;
}
return len;
}
接下来是头节点有数据的
int LinkListLen(const ListNode &link_list){
Node* p = link_list;
if(NULL == p) return 0;
int len = 1;
while(NULL != p->next){
p = p->next;
len++;
}
return len;
}
只需要修改最开始的初始指针就可以了。
4.查找元素
查找元素的话,链表不具有随机访问的特性,每次都不要从头节点进行遍历,所以时间复杂度为O(n)
Node* FindNode(const ListNode &link_list, int val){
Node *p = link_list->next;
if(NULL == p) return NULL;
while(NULL != p->next){
if(p->data == val) return p;
p = p->next;
}
return NULL;
}
同样的,另一种实现方式如下
Node* FindNode(const ListNode &link_list, int val){
Node *p = link_list; //只需要修改此处即可
if(NULL == p) return NULL;
while(NULL != p->next){
if(p->data == val) return p;
p = p->next;
}
return NULL;
}
5.插入元素
- 头部插入元素:在链表的第1个节点之前插入新节点O(1)
- 尾部插入元素:在链表最后1个节点之后插入新节点O(n)
- 链表中间插入元素:在第i个节点之前插入新节点O(n)
头节点不存储数据,那么头部插入和链表中间插入元素的方法是一样的,如下所示
void InsertRear(ListNode &link_list, int val){
Node *p = link_list;
while(NULL != p->next){
p = p->next;
}
p->next = new Node(val);
}
void InsertInside(ListNode &link_list,int index, int val){
int count = 0;
Node *p = link_list;
while(NULL != p){
if(count == index){
Node *temp = new Node(val);
temp->next = p->next;
p->next = temp;
return;
}
p = p->next;
count++;
}
cout<<"插入失败"<<endl;
}
下面我们再看头部存储数据
void InsertFront(ListNode &link_list, int val){
Node *p = new Node(val);
p->next = link_list;
link_list = p;
}
void InsertRear(ListNode &link_list, int val){
Node *p = link_list;
while(NULL != p->next){
p = p->next;
}
p->next = new Node(val);
}
void InsertInside(ListNode &link_list,int index, int val){
if(index == 0){
InsertFront(link_list, val);
return;
}
int count = 0;
Node *p = link_list;
while(NULL != p){
if(count == index-1){
Node *temp = new Node(val);
temp->next = p->next;
p->next = temp;
return;
}
p = p->next;
count++;
}
cout<<"index超出索引"<<endl;
}
头部存储数据的话,则需要考虑三种情况,并且头部插入数据时,需要注意,这里是将新创建节点的指针赋值给原来的链表头节点。
如果是指针解引用的数值,你在函数中修改它,原来指针指向的数据就会改变,但是如果你修改指针的地址,那么这个指针变量属于“形参”,所以这里要么采用**的双重指针,或者加上&,使用引用(这里ListNode已经包含了一重指针)。
对于插入中间元素,如果采用头节点不存储数据,那么InsertInside就可以涵盖所有情况
而采用头节点存储数据,InsertInside还需要额外考虑头部的插入。
6.修改元素
将第i个元素的值修改为val,因为需要遍历,所以操作平均时间复杂度是 O(n),所以该算法的时间复杂度是 O(n)。
void ChangeNode(ListNode &link_list,int index, int val){
int count = 0;
Node *p = link_list->next;
if(NULL == p){
cout << "链表为空" << endl;
return;
}
while(NULL != p->next){
if(count == index){
p->data = val;
return;
}
p = p->next;
count++;
}
cout<<"修改失败"<<endl;
}
而对于头节点存储数据的话,也差不多
void ChangeNode(ListNode &link_list,int index, int val){
int count = 0;
Node *p = link_list; //修改此处即可
if(NULL == p){
cout << "链表为空" << endl;
return;
}
while(NULL != p->next){
if(count == index){
p->data = val;
return;
}
p = p->next;
count++;
}
cout<<"index超出索引"<<endl;
}
7.删除元素
- 链表头部删除元素:删除链表的第
1
个链节点。O(1) - 链表尾部删除元素:删除链表末尾最后
1
个链节点。O(n) - 链表中间删除元素:删除链表第
i
个链节点。O(n)
同样的,头节点不存储数据的话,插入元素不需要考虑头部插入和尾部插入
void RemoveRear(ListNode &link_list){
Node *p = link_list->next;
if(NULL == p){
cout << "链表为空" << endl;
return;
}else if(NULL == p->next){
delete p;
link_list->next = NULL;
}
while(NULL != p->next->next){
p = p->next;
}
delete p->next;
p->next = NULL;
}
void RemoveInside(ListNode &link_list, int index){
Node *p = link_list;
int count = 0;
while(NULL != p->next){
if(count == index){
Node* temp = p->next->next;
delete p->next;
p->next = temp;
return;
}
p = p->next;
count++;
}
cout << "index 超出索引" << endl;
}
如果头节点存储数据,那么还是需要讨论删除头节点的情况
void RemoveFront(ListNode &link_list){
Node *p = link_list;
if(NULL == p){
cout << "链表为空" << endl;
return;
}
Node* temp = p->next;
delete link_list;
link_list = temp;
}
void RemoveRear(ListNode &link_list){
Node *p = link_list;
if(NULL == p){
cout << "链表为空" << endl;
return;
}else if(NULL == p->next){
delete link_list;
link_list = NULL;
}
while(NULL != p->next->next){
p = p->next;
}
delete p->next;
p->next = NULL;
}
void RemoveInside(ListNode &link_list, int index){
if(index == 0){
RemoveFront(link_list);
return;
}
Node *p = link_list;
int count = 0;
if(NULL == p){
cout << "链表为空" << endl;
return;
}
while(NULL != p->next){
if(count == index-1){
Node* temp = p->next->next;
delete p->next;
p->next = temp;
return;
}
p = p->next;
count++;
}
cout << "index 超出索引" << endl;
}
单链表总结
链表最大的优点在于可以灵活添加和删除元素,进行头部插入、头部删除元素操作的时间复杂度是 O(1),相比于数组而言
- 数组的随机访问,即按索引访问能力强。
- 数组的尾部插入和删除更快。
但是数组的插入和删除都伴随这多次的位移操作,而链表则只需要找准节点之后,操作一次即可。
老规矩,如果有用,希望大家的二连,感谢感谢!