单向链表的两种实现
第一种方法:
此方法较为典型,便于理解。添加、插入、删除的复杂度都是O(N).
//单向链表(一)
#include<iostream>
using namespace std;
struct Node//节点结构体
{
int Data;
Node* next;
};
class Link//链表类
{
public:
int length;
Node *head;//头节点
Link()//构造方法
{
length=0;
head=NULL;
}
void add(int a)//目的:向链表末尾插入一个节点,该节点data值为a
/*实现方法:通过头结点遍历链表,找到尾节点,
new一个节点对象,把尾节点的next指针指向这个新对象 */
{
++length;
if(head==NULL)
{
head=new Node;
head->Data=a;
head->next=NULL;
}
else
{
Node *temp=head;
while(temp->next!=NULL)
{
temp=temp->next;
}
temp->next=new Node;//申请空间
temp=temp->next;
temp->Data=a;
temp->next=NULL;
}
}
void insert(int a,int index){
//目的:向第index个元素后(不是下标) 插入一个新节点,其值为a
/*实现方法:通过头节点搜索链表,直到第找到第index个节点,
new一个节点对象,将第index节点的next指针指向该对象 */
--index;
if(head==NULL||index>length){
add(a);
++length;
return;
}
else{
++length;
int count=0;
Node* temp=head;
while(count<index){
++count;
temp=temp->next;
}
Node *plus=new Node;
plus->Data=a;
plus->next=temp->next;
temp->next=plus;
}
}
void remove(int index){
//目的:删除一个节点
//实现方法:找到第index个节点的上一个节点,把它的next指针指向 第index节点的下一个节点。
if(index>length){
cout<<"没了\n";
return;
}
int count=0;
Node* temp=head;
while(count<index-1){
++count;
temp=temp->next;
}
Node *del=temp->next->next;
temp->next=del;
--length;
}
};
int main()
{
Link list;
for(int i=1; i<=9; ++i)
{
list.add(i);
}
cout<<"节点数目--"<<list.length<<'\n';
Node *temp=list.head;
while(temp!=NULL){
cout<<temp->Data<<'\n';
temp=temp->next;
}
cout<<"=================================\n";
list.insert(100,3);
temp=list.head;
while(temp!=NULL){
cout<<temp->Data<<'\n';
temp=temp->next;
}
cout<<"=================================\n";
list.remove(3);
temp=list.head;
while(temp!=NULL){
cout<<temp->Data<<'\n';
temp=temp->next;
}
}
第二种方法
下面这种方法相对于第一种,增加了一个尾节点。利用这个尾节点,我将add部分做了一些优化使得向末尾插入时能实现O(1)的复杂度。事实上,借助这种思想还有其他可以实现的优化,这里只是提一下,其他的希望读者能自己动手实现。
//单向链表(一)
#include<iostream>
using namespace std;
struct Node//节点结构体
{
int Data;
Node* next;
};
class Link//链表类
{
public:
int length;
Node *head;//头节点
Node *last;//尾节点
Link()//构造方法
{
length=0;
head=NULL;
last=head;
}
void add(int a)//目的:向链表末尾插入一个节点,该节点data值为a
/*实现方法:直接向尾节点后加一个新的节点,复杂度为O(1) */
{
++length;
if(head==NULL)
{
head=new Node;
head->Data=a;
head->next=NULL;
last=head;//链表中只有一个节点,既是头也是尾
}
else
{
Node *temp=new Node;//新建节点,存a
temp->Data=a;
temp->next=NULL;
last->next=temp;
last=temp;
}
}
void insert(int a,int index){
//目的:向第index个元素后(不是下标) 插入一个新节点,其值为a
/*实现方法:通过头节点搜索链表,直到第找到第index个节点,
new一个节点对象,将第index节点的next指针指向该对象 */
--index;
if(head==NULL||index>length){
add(a);
++length;
return;
}
else{
++length;
int count=0;
Node* temp=head;
while(count<index){
++count;
temp=temp->next;
}
Node *plus=new Node;
plus->Data=a;
plus->next=temp->next;
temp->next=plus;
}
}
void remove(int index){
//目的:删除一个节点
//实现方法:找到第index个节点的上一个节点,把它的next指针指向 第index节点的下一个节点。
if(index>length){
cout<<"没了\n";
return;
}
int count=0;
Node* temp=head;
while(count<index-1){
++count;
temp=temp->next;
}
Node *del=temp->next->next;
temp->next=del;
--length;
}
};
int main()
{
Link list;
for(int i=1; i<=9; ++i)
{
list.add(i);
}
cout<<"节点数目--"<<list.length<<'\n';
Node *temp=list.head;
while(temp!=NULL){
cout<<temp->Data<<'\n';
temp=temp->next;
}
cout<<"=================================\n";
list.insert(100,3);
temp=list.head;
while(temp!=NULL){
cout<<temp->Data<<'\n';
temp=temp->next;
}
cout<<"=================================\n";
list.remove(3);
temp=list.head;
while(temp!=NULL){
cout<<temp->Data<<'\n';
temp=temp->next;
}
}
后面会继续写一下双向链表和其他的一些有趣的算法,既作为自身基础知识的巩固,也起到小小的科普作用
单向链表的排序算法
众所周知,排序算法多种多样,但常写的快排和归并一般都是针对数组的版本。那对于链表应该怎样排序呢?其实道理是一样的,只是实现起来有些细节上的不同。
先看看快排。
快速排序
- 快排很重要的一点是找基准点,在链表中很容易实现,直接取头节点就好。
- 在数组中,进行一轮基准定位排序是很方便的,直接从最右端开始 --,再从最左端开始 ++,通过下标遍历。可是对于单向链表,没有办法通过子节点去找父节点,也就是说不知道如何分割链表。回顾一下上文,我们只知道左子链的左端和右子链的右端在哪,换言之,所要求的是左子链的右端和右子链的左端,中间夹着的是基准点。
- 这时就需要大家发动一下想象力,构造两个辅助指针p1和p2。其中p1作为左子链的尾节点(右端),p2就是右子链的头节点(左端)
具体操作: 在搜索的时候只动p2,当p2找到比基准值小的节点时,将p1向右移动一位(相当于给左子链扩容),然后交换p1和p2。当搜索到末尾时,即p2=NULL,和数组快排一样,需要把head和p1换位。
代码如下:
void swap(Node* p1,Node* p2){
Node t;
t.Data=p1->Data;
p1->Data=p2->Data;
p2->Data=t.Data;
}
void qsort(Node* head,Node* tail){
if(head==NULL||head==tail){
return;
}
int datum=head->Data;//基准点数值
Node*p1=head;
Node*p2=head->next;
while(p2!=tail){
if(p2->Data<datum){//找到了比基准点小的节点,应放到基准点左边
p1=p1->next;//先扩容
swap(p1,p2);
}
p2=p2->next;
}
swap(head,p1);//把头结点移到基准点位置
qsort(head,p1);//对左链排序
qsort(p1->next,tail);//对右链排序
}
这里还给大家介绍一个数值交换的小技巧,通过异或位运算实现。
伪代码是:a=a ^ b ; b=a ^ b ; a=a^b;
还可以简化成:a^ =b^ =a^ =b;、
但要清楚的是,这种位运算对于提高性能用处不大,也降低了可读性。但能让你看起来很专业
归并排序
相比于数组,链表在插入和删除上的表现十分优异,很适于归并。
在讲解链表归并之前,先回忆一下数组归并。
这篇博客讲得很好 > > > https://www.cnblogs.com/chengxiao/p/6194356.html < < <
归并算法是分治思想的鲜明体现,先从终点开始,用递归把数组分成一个个数字并排序(单独的数字一定有序),再开始合并,并起来时是把小的放左边,大的放右边,如果有一个数组空了,就把另一个全部放进来。
那么现在的问题分成了两个:
- 如何从中点切开链表
- 如何合并链表
第一个问题需要开一下脑洞,使用一个叫做“快慢指针”的方法。
首先两个指针都指向head,快指针一次走两格,慢指针一次走一格,那么当快指针指到最后一个节点时,即为NULL时,由于慢指针速度只有快指针的一半,慢指针刚好走到中点。
借助这样的特性就可以定位链表的中点了
void mergeLink(Node* first,Node* r_mid, Node* last){
Link temp;
Node* i=first, *j=r_mid;
while(i!=NULL&&j!=NULL){
if(i->Data<j->Data){
temp.add(i->Data);
i=i->next;
}
else{
temp.add(j->Data);
j=j->next;
}
}
while(i!=NULL){
temp.add(i->Data);
i=i->next;
}
i=r_mid;
while(j!=NULL){
temp.add(j->Data);
j=j->next;
}
*first=*temp.head;//让first直接指向拼好的链表
}
void mergeSort(Node* head){
if(!head||!head->next){
return;
}
Node* slow=head,*fast=head->next;
//如果让fast指向head,那么最后slow本身就是中点,
//在删除slow的时候,只能使slow自身变为NULL,而它的上一位next的指向并不为空
while(fast&&fast->next){
fast=fast->next->next;
slow=slow->next;
}
Node* right=slow->next;
mergeSort(right);//右链排序
slow->next=NULL;//切断
Node* left=head;
mergeSort(left);//左链排序
mergeLink(left,right,NULL);//合并
// 左起点 右起点 终点
}
关于快慢指针 其实还有很多别的用途,推荐大家看下这篇博客
快慢指针应用总结 > > > https://blog.csdn.net/qq_21815981/article/details/79833976 < < <