面试算法 牛客题目 单链表的排序

1.题目:   单链表的排序
描述
给定一个节点数为n的无序单链表,对其按升序排序。

数据范围:0 < n \le 1000000<n≤100000
要求:空间复杂度 O(n),时间复杂度 O(nlogn)


 



2.算法:
1.暴力算法 .数组赋值的方法  
 2.归并排序(推荐使用) (因为时间复杂度 和空间复杂度  最小) 


3.算法思想:

1.暴力算法 .数组赋值的方法  

把链表里面的 值 存取 动态数组 vector  里面  再排序  最后赋值)

 2.归并排序(推荐使用) (因为时间复杂度 和空间复杂度  最小) 

思路:
前面我们做合并两个有序链表不是使用归并思想吗?说明在链表中归并排序也不是不可能使用,合并阶段可以参照前面这道题,两个链表逐渐取最小的元素就可以了,但是划分阶段呢?

常规数组的归并排序是分治思想,即将数组从中间个元素开始划分,然后将划分后的子数组作为一个要排序的数组,再将排好序的两个子数组合并成一个完整的有序数组,因此采用的是递归。而链表中我们也可以用同样的方式,只需要找到中间个元素的前一个节点,将其断开,就可以将链表分成两个子链表,然后继续划分,直到最小,然后往上依次合并。

终止条件: 当子链表划分到为空或者只剩一个节点时,不再继续划分,往上合并。
返回值: 每次返回两个排好序且合并好的子链表。
本级任务: 找到这个链表的中间节点,从前面断开,分为左右两个子链表,进入子问题排序。
怎么找中间元素呢?我们也可以使用快慢双指针,快指针每次两步,慢指针每次一步,那么快指针到达链表尾的时候,慢指针正好走了快指针距离的一半,为中间元素。

具体做法:

step 1:首先判断链表为空或者只有一个元素,直接就是有序的。
step 2:准备三个指针,快指针right每次走两步,慢指针mid每次走一步,前序指针left每次跟在mid前一个位置。三个指针遍历链表,当快指针到达链表尾部的时候,慢指针mid刚好走了链表的一半,正好是中间位置。
step 3:从left位置将链表断开,刚好分成两个子问题开始递归。
step 4:将子问题得到的链表合并,参考合并两个有序链表。


4.代码:

/*************************************************
作者:She001
时间:2022/9/30
题目:   单链表的排序
描述
给定一个节点数为n的无序单链表,对其按升序排序。

数据范围:0 < n \le 1000000<n≤100000
要求:空间复杂度 O(n),时间复杂度 O(nlogn)


***************************************************/
//算法:
//1.暴力算法 .数组赋值的方法  
// 2.归并排序(推荐使用) (因为时间复杂度 和空间复杂度  最小) 

#include<bits/stdc++.h>
using namespace std;
typedef struct node
{
	int i;
	node *next;	
}; 


void print(node * head)//打印链表 
{
	node* pp= head;//复制头节点 
	while(pp!=NULL)//判断这个节点是否为空  链表是否结束 
	{
		cout<<pp->i<<"  ";
		pp=pp->next;//指向下一个 
	}
	cout<<endl;
	
}



int lianbiao_num(node * head)//函数的作用 返回链表的个数
{
	int i=0;
	node* pp=head;
	while(pp!=NULL)
	{
		i++;
		pp=pp->next;	
	}
	//cout<<"链表中节点的个数: "<<i<<endl;
	return i;	
} 


node * reverseList(node* head)//翻转链表 
{
	if(head==NULL)
	{
		return NULL;	
	}	
	node * a = head;
	node * b = NULL;
	while(a!=NULL)
	{
		node * c = a->next;
		a->next=b;
		b=a;
		a=c;
	}
	return b;	
} 






//暴力算法 
//算法思想:(把链表里面的 值 存取 动态数组 vector  里面  再排序  最后赋值)
    
node * fangfa_1(node * head1)//实现的方法 
{
	vector<int> nums;
	node* a1=head1;
	//遍历节点 ,将节点的值加入动态数组
	while(a1!=NULL)
	{
		nums.push_back(a1->i);//把值 放入动态数组
		a1=a1->next;	
	} 
	a1=head1;
	//数组排序
	sort(nums.begin(),nums.end());//参数1  动态数组的开头   参数二   动态数组的 结尾 
	//遍历数组,赋值
	int i=0;
	while(a1!=NULL)
	{
		a1->i=nums[i++];//赋值 
		a1=a1->next;//指向下一个节点	
	} 
	return head1;//返回头节点 
} 

//归并排序(推荐使用) (因为时间复杂度 和空间复杂度  最小) 
/*
算法思想:
思路:
前面我们做合并两个有序链表不是使用归并思想吗?说明在链表中归并排序也不是不可能使用,合并阶段可以参照前面这道题,两个链表逐渐取最小的元素就可以了,但是划分阶段呢?

常规数组的归并排序是分治思想,即将数组从中间个元素开始划分,然后将划分后的子数组作为一个要排序的数组,再将排好序的两个子数组合并成一个完整的有序数组,因此采用的是递归。而链表中我们也可以用同样的方式,只需要找到中间个元素的前一个节点,将其断开,就可以将链表分成两个子链表,然后继续划分,直到最小,然后往上依次合并。

终止条件: 当子链表划分到为空或者只剩一个节点时,不再继续划分,往上合并。
返回值: 每次返回两个排好序且合并好的子链表。
本级任务: 找到这个链表的中间节点,从前面断开,分为左右两个子链表,进入子问题排序。
怎么找中间元素呢?我们也可以使用快慢双指针,快指针每次两步,慢指针每次一步,那么快指针到达链表尾的时候,慢指针正好走了快指针距离的一半,为中间元素。

具体做法:

step 1:首先判断链表为空或者只有一个元素,直接就是有序的。
step 2:准备三个指针,快指针right每次走两步,慢指针mid每次走一步,前序指针left每次跟在mid前一个位置。三个指针遍历链表,当快指针到达链表尾部的时候,慢指针mid刚好走了链表的一半,正好是中间位置。
step 3:从left位置将链表断开,刚好分成两个子问题开始递归。
step 4:将子问题得到的链表合并,参考合并两个有序链表。
*/


//合并两段有序链表
node* merge(node* pHead1, node* pHead2) { 
    //一个已经为空了,直接返回另一个
    if(pHead1 == NULL) 
        return pHead2;
    if(pHead2 == NULL)
        return pHead1;
    //加一个表头
    node* head = new node; 
    node* cur = head;
    //两个链表都要不为空
    while(pHead1 && pHead2){ 
        //取较小值的节点
        if(pHead1->i <= pHead2->i){ 
            cur->next = pHead1;
            //只移动取值的指针
            pHead1 = pHead1->next; 
        }else{
            cur->next = pHead2;
            //只移动取值的指针
            pHead2 = pHead2->next; 
        }
        //指针后移
        cur = cur->next; 
    }
    //哪个链表还有剩,直接连在后面
    if(pHead1) 
        cur->next = pHead1;
    else
        cur->next = pHead2;
    //返回值去掉表头
    return head->next; 
}

node* fangfa_2(node* head)   //排序链表 
{
    //链表为空或者只有一个元素,直接就是有序的
    if(head == NULL || head->next == NULL) //预防传入的下一个排序 的两个头节点有错误! 保证有每个链表有一个元素 
        return head;
    node* left = head; 
    node* mid = head->next;
    node* right = head->next->next;
    //右边的指针到达末尾时,中间的指针指向该段链表的中间
    while(right != NULL && right->next != NULL) //分为两个部分  平分 
	{ 
        left = left->next;
        mid = mid->next;
        right = right->next->next;
    }
    //左边指针指向左段的左右一个节点,从这里断开
    left->next = NULL; 
    //分成两段排序,合并排好序的两段
    return merge(fangfa_2(head), fangfa_2(mid)); 
}




int main()
{
	
	
	//建立  第一个 单链表  
	node *a1=new node;
	node *a2=new node;
	node *a3=new node;
	node *a4=new node;
	node *a5=new node;
	node *a6=new node;
	node *a7=new node;
	node *a8=new node;
	node *a9=new node; 
	
	a1->i=2;//链表节点的复制 
	a2->i=1;
	a3->i=4;
	a4->i=3;
	a5->i=7;
	a6->i=8;
	a7->i=9;
	a8->i=6;
	a9->i=5;
	
	
	a1->next=a2;//链表的连接 
	a2->next=a3;
	a3->next=a4;
	a4->next=a5;
	a5->next=a6;
	a6->next=a7;
	a7->next=a8;
	a8->next=a9;//a5 是 两个链表的节点 
	a9->next=NULL;
	
	//node* gg= fangfa_1(a1);
	//print(gg);
	
	node* gg1= fangfa_2(a1);
	print(gg1);
	
	
	

	return 	0;
} 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值