19 删除链表倒数第N个节点
中等题
算是链表题打基础。
单链表是不能往回走的。如果数据结构里没有存储链表长度,那只能靠遍历来获得。一种想法就是先遍历获得链表长度,再遍历获得删除位置。需要遍历两次。第二种方法是遍历后存储,借助一个栈来把遍历到的节点存储后最后再弹出,不仅花费空间多在时间上也得经过入栈-出栈的过程。第三种是快慢指针,通过相距为n的指针同时运动,当前一个指针到达表尾,后一个指针正好到达需要删除的地方。这样只要进行一趟循环。当然,这相当于对复杂度O(n)前的常数进行修改,本质上并没有很大提升。
#include <iostream>
using namespace std;
struct ListNode {
int val;
ListNode *next;
ListNode() : val(0), next(nullptr) {}
ListNode(int x) : val(x), next(nullptr) {}
ListNode(int x, ListNode *next) : val(x), next(next) {}
};
class Solution {
public:
ListNode* removeNthFromEnd(ListNode* head, int n) {
ListNode* front = head;
ListNode* dummy = new ListNode(0, head);
ListNode* back = dummy; //下一个才是head
while(n--){
front = front->next;
}
while(front){
front = front->next;
back = back->next;
}
back->next = back->next->next;
ListNode* res = dummy->next;
return res;
}
};
int main(){
Solution st;
ListNode* head = new ListNode(1);
ListNode* cuv = new ListNode(2);
head->next = cuv;
cuv->next = new ListNode(3);
cuv = cuv->next;
cuv->next = new ListNode(4);
cuv = cuv->next;
cuv->next = new ListNode(5);
cuv = cuv->next;
cuv->next = nullptr;
st.removeNthFromEnd(head,5);
return 0;
}
这里为了不处理头指针的特殊情况(如果慢指针一开始就指向头指针,快指针会越界),而插入dumpy哑指针。这个dumpy本身的数据并不起作用,只是使得慢指针和快指针起初就相差1个单位。不过按照题解并没有释放删除的元素,难道这样不会内存泄漏吗?
20 有效的括号
简单题
括号匹配是经典的栈的应用。也许有其他形式很简单的代码写法,但是效率一定是栈是最高的(或者说那些写法也只不过是封装了栈的操作,例如python的某些函数)。
简单题,直接贴代码:
#include <iostream>
#include <stack>
using namespace std;
class Solution {
public:
bool isValid(string s) {
stack<char> stk;
for(int i=0; i<s.size(); i++){
//以下是字符串仍在匹配的情况讨论
//情况一:栈不为空
if(!stk.empty()){
if((stk.top() == '(' && s[i] == ')') || (stk.top() == '{' && s[i] == '}') || (stk.top() == '[' && s[i] == ']')){
//栈顶元素和下一个字符匹配
stk.pop();
}
else{
if(s[i] == '(' || s[i] == '[' || s[i] == '{'){
//还能继续匹配
stk.push(s[i]);
}
else return false;
}
}
//情况二:栈为空
else{
//还能继续匹配
if(s[i] == '(' || s[i] == '[' || s[i] == '{') stk.push(s[i]);
//匹配不了了
else return false;
}
}
//此时字符串已经匹配完了
if(!stk.empty()) return false;
else return true;
}
};
int main(){
Solution st;
string s = "()";
cout<<st.isValid(s)<<endl;
return 0;
}
21 合并两个有序链表
简单题
对于链表的基本操作训练。即使不是链表也能使用迭代的方法,对表项进行逐个比对,插入正确的位置。每次需要对比的位置是<list1和list2未计入输出的下一个位置>,取其中小的节点接入输出。
#include <iostream>
using namespace std;
struct ListNode {
int val;
ListNode *next;
ListNode() : val(0), next(nullptr) {}
ListNode(int x) : val(x), next(nullptr) {}
ListNode(int x, ListNode *next) : val(x), next(next) {}
};
class Solution {
public:
ListNode* mergeTwoLists(ListNode* list1, ListNode* list2) {
if(!list1) return list2;
if(!list2) return list1;
ListNode* cuv1 = list1; //指向list1中未匹配的部分
ListNode* cuv2 = list2; //指向list2中未匹配的部分
ListNode* res = list1->val < list2->val ? list1 : list2; //指向小的一边,其实也确定了第一个输出节点
ListNode* cur = res;
if(cuv1->val < cuv2->val) cuv1 = cuv1->next;
else cuv2 = cuv2->next; //确定了第一个输出节点,相应的要往后移动
while(cuv1 && cuv2){
if(cuv1->val < cuv2->val){
//cur结果指针指向cuv1,cuv1向后移动一位
cur->next = cuv1;
cur = cuv1;
cuv1 = cuv1->next;
}
else{
cur->next = cuv2;
cur = cuv2;
cuv2 = cuv2->next;
}
}
//处理某个链表过剩的情况
if(!cuv1) cur->next = cuv2;
else cur->next = cuv1;
return res;
}
};
int main(){
Solution st;
ListNode* list1 = new ListNode(1,new ListNode(2,new ListNode(4)));
ListNode* list2 = new ListNode(1,new ListNode(3,new ListNode(4)));
st.mergeTwoLists(list1,list2);
return 0;
}
以上是使用迭代的方法,时间复杂度为一次项即O(m+n)。链表操作往往拥有递归解法,因为链表具有递归的特性。按照递归的方式思考,本函数将两个升序链表输入,返回一个升序链表。注意:输出的并非一个全新的链表,而是通过连接已有的节点而形成的链表。这就允许使用递归了,因为递归主要就在于找一个递推。这里的递推条件很简单:
- list1的当前节点的值小于list2当前节点的值:说明list1当前节点的值是这两个链表中最小的,因此连接上这个节点作为输出。该节点的next指针指向除去该节点的两个链表进行合并操作得到的节点。
- list1的当前节点的值大于list2当前节点的值:连接list2节点作为输出,其余类似。
class Solution {
public:
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {
if (l1 == nullptr) {
return l2;
} else if (l2 == nullptr) {
return l1;
} else if (l1->val < l2->val) {
l1->next = mergeTwoLists(l1->next, l2);
return l1;
} else {
l2->next = mergeTwoLists(l1, l2->next);
return l2;
}
}
}
递归在空间复杂度上略逊于迭代。但是作为一种重要的思想,还是在这里列出了。