BM11.链表相加(二)
题目描述:
- 给定两个链表,每个链表中节点值都是0-9,每个链表就可以表示一个数字
- 将两个链表表示的数字相加,结果也存在链表中
方法:反转链表法(推荐使用)
思路:
既然链表每个节点表示数字的每一位,那相加的时候自然可以按照加法法则,从后往前依次相加。但是,链表是没有办法逆序访问的,这是我们要面对第一只拦路虎。解决它也很简单,既然从后往前不行,那从前往后总是可行的吧,将两个链表反转一 下:
1 2 3 4 5 6 7 8 9 10 |
|
即可得到个十百千……各个数字从前往后的排列,相加结果也是个位在前,怎么办?再次反转,结果不就正常了。
具体做法:
- step 1:任意一个链表为空,返回另一个链表就行了,因为链表为空相当于0,0加任何数为0,包括另一个加数为0的情况。
- step 2:相继反转两个待相加的链表,反转过程可以参考反转链表。
- step 3:设置返回链表的链表头,设置进位carry=0.
- step 4:从头开始遍历两个链表,直到两个链表节点都为空且carry也不为1. 每次取出不为空的链表节点值,为空就设置为0,将两个数字与carry相加,然后查看是否进位,将进位后的结果(对10取模)加入新的链表节点,连接在返回链表后面,并继续往后遍历。
- step 5:返回前将结果链表再反转回来。
class Solution {
public:
//反转链表
ListNode* ReverseList(ListNode* pHead) {
if(pHead == NULL)
return NULL;
ListNode* cur = pHead;
ListNode* pre = NULL;
while(cur != NULL){
//断开链表,要记录后续一个
ListNode* temp = cur->next;
//当前的next指向前一个
cur->next = pre;
//前一个更新为当前
pre = cur;
//当前更新为刚刚记录的后一个
cur = temp;
}
return pre;
}
ListNode* addInList(ListNode* head1, ListNode* head2) {
//任意一个链表为空,返回另一个
if(head1 == NULL)
return head2;
if(head2 == NULL)
return head1;
//反转两个链表
head1 = ReverseList(head1);
head2 = ReverseList(head2);
//添加表头
ListNode* res = new ListNode(-1);
ListNode* head = res;
//进位符号
int carry = 0;
//只要某个链表还有或者进位还有
while(head1 != NULL || head2 != NULL || carry != 0){
//链表不为空则取其值
int val1 = head1 == NULL ? 0 : head1->val;
int val2 = head2 == NULL ? 0 : head2->val;
//相加
int temp = val1 + val2 + carry;
//获取进位
carry = temp / 10;
temp %= 10;
//添加元素
head->next = new ListNode(temp);
head = head->next;
//移动下一个
if(head1 != NULL)
head1 = head1->next;
if(head2 != NULL)
head2 = head2->next;
}
//结果反转回来
return ReverseList(res->next);
}
};
BM12.单链表的排序
题目描述
给定一个节点数为n的无序单链表,对其按升序排序。
数据范围:0<�≤1000000<n≤100000,保证节点权值在[−109,109][−109,109]之内。
要求:空间复杂度O(n),时间复杂度O(nlogn)
方法一:归并排序(推荐使用)
知识点1:分治
分治即“分而治之”,“分”指的是将一个大而复杂的问题划分成多个性质相同但是规模更小的子问题,子问题继续按照这样划分,直到问题可以被轻易解决;“治”指的是将子问题单独进行处理。经过分治后的子问题,需要将解进行合并才能得到原问题的解,因此整个分治过程经常用递归来实现。
知识点2:双指针
双指针指的是在遍历对象的过程中,不是普通的使用单个指针进行访问,而是使用两个指针(特殊情况甚至可以多个),两个指针或是同方向访问两个链表、或是同方向访问一个链表(快慢指针)、或是相反方向扫描(对撞指针),从而达到我们需要的目的。
思路:
前面我们做合并两个有序链表不是使用归并思想吗?说明在链表中归并排序也不是不可能使用,合并阶段可以参照前面这道题,两个链表逐渐取最小的元素就可以了,但是划分阶段呢?
常规数组的归并排序是分治思想,即将数组从中间个元素开始划分,然后将划分后的子数组作为一个要排序的数组,再将排好序的两个子数组合并成一个完整的有序数组,因此采用的是递归。而链表中我们也可以用同样的方式,只需要找到中间个元素的前一个节点,将其断开,就可以将链表分成两个子链表,然后继续划分,直到最小,然后往上依次合并。
- 终止条件: 当子链表划分到为空或者只剩一个节点时,不再继续划分,往上合并。
- 返回值: 每次返回两个排好序且合并好的子链表。
- 本级任务: 找到这个链表的中间节点,从前面断开,分为左右两个子链表,进入子问题排序。
怎么找中间元素呢?我们也可以使用快慢双指针,快指针每次两步,慢指针每次一步,那么快指针到达链表尾的时候,慢指针正好走了快指针距离的一半,为中间元素。
具体做法:
- step 1:首先判断链表为空或者只有一个元素,直接就是有序的。
- step 2:准备三个指针,快指针right每次走两步,慢指针mid每次走一步,前序指针left每次跟在mid前一个位置。三个指针遍历链表,当快指针到达链表尾部的时候,慢指针mid刚好走了链表的一半,正好是中间位置。
- step 3:从left位置将链表断开,刚好分成两个子问题开始递归。
- step 4:将子问题得到的链表合并,参考合并两个有序链表。
class Solution {
public:
//合并两段有序链表
ListNode* merge(ListNode* pHead1, ListNode* pHead2) {
//一个已经为空了,直接返回另一个
if(pHead1 == NULL)
return pHead2;
if(pHead2 == NULL)
return pHead1;
//加一个表头
ListNode* head = new ListNode(0);
ListNode* cur = head;
//两个链表都要不为空
while(pHead1 && pHead2){
//取较小值的节点
if(pHead1->val <= pHead2->val){
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;
}
ListNode* sortInList(ListNode* head) {
//链表为空或者只有一个元素,直接就是有序的
if(head == NULL || head->next == NULL)
return head;
ListNode* left = head;
ListNode* mid = head->next;
ListNode* 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(sortInList(head), sortInList(mid));
}
};
复杂度分析:
- 时间复杂度:O(nlog2n),每级划分最坏需要遍历链表全部元素,因此为O(n),每级合并都是将同级的子问题链表遍历合并,因此也为O(n),分治划分为二叉树型,一共有O(log2n)层,因此复杂度为O((n+n)∗log2n)=O(nlog2n)
- 空间复杂度:O(log2n),递归栈的深度最坏为树型递归的深度,log2n层
方法二:转化为数组排序(扩展思路)
思路:
链表最难受的就是不能按照下标访问,只能逐个遍历,那像排序中常规的快速排序、堆排序都不能用了,只能用依次遍历的冒泡排序、选择排序这些。但是这些O(n2)复杂度的排序方法太费时间了,我们可以将其转化成数组后再排序。
具体做法:
- step 1:遍历链表,将节点值加入数组。
- step 2:使用内置的排序函数对数组进行排序。
- step 3:依次遍历数组和链表,按照位置将链表中的节点值修改为排序后的数组值。
class Solution {
public:
ListNode* sortInList(ListNode* head) {
vector<int> nums;
ListNode* p = head;
//遍历链表,将节点值加入数组
while(p != NULL){
nums.push_back(p->val);
p = p->next;
}
p = head;
//对数组元素排序
sort(nums.begin(), nums.end());
//遍历数组
for(int i = 0; i < nums.size(); i++){
//将数组元素依次加入链表
p->val = nums[i];
p = p->next;
}
return head;
}
};
复杂度分析:
- 时间复杂度:O(nlog2n),sort函数一般为优化后的快速排序,复杂度为O(nlog2n)
- 空间复杂度:O(n),存储链表元素值的辅助数组长度n
收获总结:
1.分治法的思想非常好,结合递归使用。
1.第二种方法很妙,注意并不需要新建链表,而是可以直接遍历修改链表元素即可。
BM13.判断一个链表是否为回文结构
方法一:数组复制反转法(前置知识)
思路:
即然回文结构正序遍历和逆序遍历结果都是一样的,我们是不是可以尝试将正序遍历的结果与逆序遍历的结果一一比较,如果都是对应的,那很巧了!它就是回文结构!
这道题看起来解决得如此之快,但是别高兴太早,链表可没有办法逆序遍历啊。链表由前一个节点的指针指向后一个节点,指针是单向的,只能从前面到后面,我们不能任意访问,也不能从后往前。但是,另一个容器数组,可以任意访问,我们把链表中的元素值取出来放入数组中,然后判断数组是不是回文结构,这不是一样的吗?
具体做法:
- step 1:遍历一次链表,将元素取出放入辅助数组中。
- step 2:准备另一个辅助数组,录入第一个数组的全部元素,再将其反转。
- step 3:依次遍历原数组与反转后的数组,若是元素都相等则是回文结构,只要遇到一个不同的就不是回文结构。
class Solution {
public:
bool isPail(ListNode* head) {
vector<int> nums;
//将链表元素取出一次放入数组
while(head != NULL){
nums.push_back(head->val);
head = head->next;
}
vector<int> temp = nums;
//准备一个数组承接翻转之后的数组
reverse(temp.begin(), temp.end());
for(int i = 0; i < nums.size(); i++){
//正向遍历与反向遍历相同
if(nums[i] != temp[i])
return false;
}
return true;
}
};
复杂度分析:
- 时间复杂度:O(n),其中n为链表的长度,遍历链表转化数组为O(n),反转数组为O(n),后续遍历两个数组为O(n)
- 空间复杂度:O(n),记录链表元素的辅助数组,及记录反转后数组
方法二:数组复制双指针(前置知识)
知识点:双指针
双指针指的是在遍历对象的过程中,不是普通的使用单个指针进行访问,而是使用两个指针(特殊情况甚至可以多个),两个指针或是同方向访问两个链表、或是同方向访问一个链表(快慢指针)、或是相反方向扫描(对撞指针),从而达到我们需要的目的。
思路:
既然方法一我们已经将链表的值放入了数组中,数组是可以按照下标直接访问的,那干啥还要傻乎乎地用另一个数组来表示反转后的数组呢?我们直接从后往前遍历与从前往后遍历一同比较,利用两个指针对撞访问,不就少了很多额外的时间了吗?
具体做法:
- step 1:遍历一次链表,将元素取出放入辅助数组中。
- step 2:使用下标访问,两个下标代表两个指针,两个指针分别从数组首尾开始遍历,左指针指向开头,从左到右,右指针指向数组末尾,从右到左,依次比较元素是否相同。
- step 3:如果有不一样,则不是回文结构。否则遍历到两个指针相遇就好了,因为左指针到了右半部分都是右指针走过的路,比较的值也是与之前相同的。
class Solution {
public:
bool isPail(ListNode* head) {
vector<int> nums;
//将链表元素取出一次放入数组
while(head != NULL){
nums.push_back(head->val);
head = head->next;
}
//双指针指向首尾
int left = 0;
int right = nums.size() - 1;
//分别从首尾遍历,代表正序和逆序
while(left <= right){
//如果不一致就是不为回文
if(nums[left] != nums[right])
return false;
left++;
right--;
}
return true;
}
};
复杂度分析:
- 时间复杂度:O(n),其中n为链表的长度,遍历链表转化数组为O(n),双指针遍历半个数组为O(n)
- 空间复杂度:O(n),记录链表元素的辅助数组
方法三:长度法找中点(推荐使用)
知识点:双指针
双指针指的是在遍历对象的过程中,不是普通的使用单个指针进行访问,而是使用两个指针(特殊情况甚至可以多个),两个指针或是同方向访问两个链表、或是同方向访问一个链表(快慢指针)、或是相反方向扫描(对撞指针),从而达到我们需要的目的。
思路:
在数组中,我们可以借助双指针,一个从前往遍历前一半数组,另一个从后往前遍历后一半数组,依次比较值。链表中如果我们要用这样的思想,左指针从前往后很容易,直接的链表的遍历就可以了。但是右指针是真的没有办法从尾巴往前走,要是链表后半段的指针是逆序的就好了。
怎么样能让链表后半段的指针反过来,将后半段链表整体反转不就行了吗?如果我们将后半段链表整体反转,那么相当于后半段就是从末尾指向中间,就可以实现后半段的逆序遍历——按照指针直接走就可以了。
class Solution {
public:
//反转链表指针
ListNode* reverse(ListNode* head) {
//前序节点
ListNode* prev = NULL;
while(head != NULL){
//断开后序
ListNode* next = head->next;
//指向前序
head->next = prev;
prev = head;
head = next;
}
return prev;
}
bool isPail(ListNode* head) {
ListNode* p = head;
int n = 0;
//找到链表长度
while(p != NULL){
n++;
p = p->next;
}
//中点
n = n / 2;
p = head;
while(n > 0){
p = p->next;
n--;
}
//中点处反转
p = reverse(p);
ListNode* q = head;
while(p != NULL){
//比较判断节点值是否相等
if(p->val != q->val)
return false;
p = p->next;
q = q->next;
}
return true;
}
};
复杂度分析:
- 时间复杂度:O(n),其中n为链表的长度,遍历链表找到长度为O(n),后续反转链表为O(n),然后再遍历两份半个链表
- 空间复杂度:O(1),常数级变量,没有额外辅助空间
方法四:双指针找中点(推荐使用)
思路:
上述方法三找中点,我们遍历整个链表找到长度,又遍历长度一半找中点位置。过程过于繁琐,我们想想能不能优化一下,一次性找到中点。
我们首先来看看中点的特征,一个链表的中点,距离链表开头是一半的长度,距离链表结尾也是一半的长度,那如果从链表首遍历到链表中点位置,另一个每次遍历两个节点的指针是不是就到了链表尾,那这时候我们的快慢双指针就登场了:
具体做法:
- step 1:慢指针每次走一个节点,快指针每次走两个节点,快指针到达链表尾的时候,慢指针刚好到了链表中点。
- step 2:从中点的位置,开始往后将后半段链表反转。
- step 3:按照方法三的思路,左右双指针,左指针从链表头往后遍历,右指针从链表尾往反转后的前遍历,依次比较遇到的值。
class Solution {
public:
//反转链表指针
ListNode* reverse(ListNode* head) {
//前序节点
ListNode* prev = NULL;
while(head != NULL){
//断开后序
ListNode* next = head->next;
//指向前序
head->next = prev;
prev = head;
head = next;
}
return prev;
}
bool isPail(ListNode* head) {
//空链表直接为回文
if(head == NULL)
return true;
ListNode* slow = head;
ListNode* fast = head;
//双指针找中点
while(fast != NULL && fast->next != NULL){
slow = slow->next;
fast = fast->next->next;
}
//中点处反转
slow = reverse(slow);
fast = head;
while(slow != NULL){
//比较判断节点值是否相等
if(slow->val != fast->val)
return false;
fast = fast->next;
slow = slow->next;
}
return true;
}
};
复杂度分析:
- 时间复杂度:O(n),其中n为链表的长度,双指针找到中点遍历半个链表,后续反转链表为O(n),然后再遍历两份半个链表
- 空间复杂度:O(1),常数级变量,没有额外辅助空间
方法五:栈逆序(扩展思路)
知识点:栈
栈是一种仅支持在表尾进行插入和删除操作的线性表,这一端被称为栈顶,另一端被称为栈底。元素入栈指的是把新元素放到栈顶元素的上面,使之成为新的栈顶元素;元素出栈指的是从一个栈删除元素又称作出栈或退栈,它是把栈顶元素删除掉,使其相邻的元素成为新的栈顶元素。
思路:
同样的,逆序访问我们不一定需要借助可以随机访问的数组,或者反转链表,我们还可以借助先进先出的栈:根据链表顺序入栈的元素,越在前面的就越在栈底,越在后面的就越在栈顶,因此后续从栈中弹出的元素,依次就是链表的逆序。
具体做法:
- step 1:遍历链表,将链表元素依次加入栈中。
- step 2:依次从栈中弹出元素值,和链表的顺序遍历比较,如果都是一一比较相同的值,那正好就是回文,否则就不是。
class Solution {
public:
bool isPail(ListNode* head) {
ListNode* p = head;
stack<int> s;
//辅助栈记录元素
while(p != NULL){
s.push(p->val);
p = p->next;
}
p = head;
//正序遍历链表,从栈中弹出的内容是逆序的
while(!s.empty()){
//比较是否相同
if(p->val != s.top())
return false;
s.pop();
p = p->next;
}
return true;
}
};
复杂度分析:
- 时间复杂度:O(n),其中n为链表的长度,遍历链表入栈为O(n),后续再次遍历链表和栈
- 空间复杂度:O(n),记录链表元素的辅助栈长度和链表相同
收获总结:
1.栈这个数据结构要合理使用。
BM14.链表的奇偶重排
方法:双指针(推荐使用)
知识点:双指针
双指针指的是在遍历对象的过程中,不是普通的使用单个指针进行访问,而是使用两个指针(特殊情况甚至可以多个),两个指针或是同方向访问两个链表、或是同方向访问一个链表(快慢指针)、或是相反方向扫描(对撞指针),从而达到我们需要的目的。
思路:
如下图所示,第一个节点是奇数位,第二个节点是偶数,第二个节点后又是奇数位,因此可以断掉节点1和节点2之间的连接,指向节点2的后面即节点3,如红色箭头。如果此时我们将第一个节点指向第三个节点,就可以得到那么第三个节点后为偶数节点,因此我们又可以断掉节点2到节点3之间的连接,指向节点3后一个节点即节点4,如蓝色箭头。那么我们再将第二个节点指向第四个节点,又回到刚刚到情况了。
1 2 3 4 5 6 7 8 |
|
这样我们就可以使用了两个同方向访问指针遍历解决这道题。
具体做法:
- step 1:判断空链表的情况,如果链表为空,不用重排。
- step 2:使用双指针odd和even分别遍历奇数节点和偶数节点,并给偶数节点链表一个头。
- step 3:上述过程,每次遍历两个节点,且even在后面,因此每轮循环用even检查后两个元素是否为NULL,如果不为再进入循环进行上述连接过程。
- step 4:将偶数节点头接在奇数最后一个节点后,再返回头部。
class Solution {
public:
ListNode* oddEvenList(ListNode* head) {
//如果链表为空,不用重排
if(head == NULL)
return head;
//even开头指向第二个节点,可能为空
ListNode* even = head->next;
//odd开头指向第一个节点
ListNode* odd = head;
//指向even开头
ListNode* evenhead = even;
while(even != NULL && even->next != NULL){
//odd连接even的后一个,即奇数位
odd->next = even->next;
//odd进入后一个奇数位
odd = odd->next;
//even连接后一个奇数的后一位,即偶数位
even->next = odd->next;
//even进入后一个偶数位
even = even->next;
}
//even整体接在odd后面
odd->next = evenhead;
return head;
}
};