一、线性表
- 1.二分法
- 2.[回文列表](https://leetcode.cn/problems/palindrome-linked-list/)
- 3.[删除列表的倒数第n个结点](https://leetcode.cn/problems/remove-nth-node-from-end-of-list/)
- 4.[合并两个有序链表](https://leetcode.cn/problems/merge-two-sorted-lists/)
- 5.[旋转链表](https://leetcode.cn/problems/rotate-list/)
- 6.[反转链表](https://leetcode.cn/problems/reverse-linked-list-ii/)
- 7.有序链表转换二叉搜索树
- 8.[环形链表](https://leetcode.cn/problems/linked-list-cycle-ii/)
- 9.[对链表进行插入排序](https://leetcode.cn/problems/insertion-sort-list/)
- 10.[链表的归并排序](https://leetcode.cn/problems/sort-list/)
- 11.[相交链表](https://leetcode.cn/problems/intersection-of-two-linked-lists/)
- 12.[删除排序链表中的重复元素](https://leetcode.cn/problems/remove-duplicates-from-sorted-list/)
- 13.[删除排序链表中的重复元素II](https://leetcode.cn/problems/remove-duplicates-from-sorted-list-ii/)
1.二分法
模板:
bool check(int x) {/* ... */} // 检查x是否满足某种性质
// 区间[l, r]被划分成[l, mid]和[mid + 1, r]时使用: //查找满足右边性质的最左元素
int bsearch_1(int l, int r)
{
while (l < r)
{
int mid = l + r >> 1;
if (check(mid)) r = mid; // check()判断mid是否满足性质
else l = mid + 1;
}
return l;
}
// 区间[l, r]被划分成[l, mid - 1]和[mid, r]时使用://查找满足左边性质的最右元素
int bsearch_2(int l, int r)
{
while (l < r)
{
int mid = l + r + 1 >> 1;
if (check(mid)) l = mid;
else r = mid - 1;
}
return l;
}
使用二分的前提是具有两段性,左侧元素具有一个性质,右侧不具有这个性质。查找左侧的最右元素和右侧的最左元素常用二分。
实例:
int l = 0, r = n-1; //查找右边大于等于x的最小元素
while(l < r){
int mid = l + r >> 1;
if(a[mid] >= x) r = mid;
else l = mid+1;
}
if(a[r]!=x) printf("-1 -1\n");//查找失败
2.回文列表
请判断一个链表是否为回文链表。
思想:
将链表一分为二,难点在于结点个数是奇数还是偶数不确定,需要判断一下。如果是奇数就跳过中间结点,否则要考虑中间结点。前后两个链表的一致可以用栈来判断,也可以用数组,倒序遍历比较就行了。
// s-慢指针 f-快指针
// 循环结束时的状态如下:
// 1 2 2 1 NULL 偶数长度 后半部分起点就是s
// s f
// else
// 1 2 3 2 1 奇数长度 后半部分起点是s的下一个
// s f
bool isPalindrome(ListNode* head) {
if(!head||!head->next) return true;//0个或1个数自然为真
stack<int> stk;//存放前一半链表
auto f = head,s = head;
while(f&&f->next){
stk.push(s->val);
s = s->next;
f = f->next->next;
}
if(f) s = s->next;//后半部分起点
while(s){
if(s->val!=stk.top()) return false;
stk.pop(),s = s->next;
}
return true;
}
3.删除列表的倒数第n个结点
给你一个链表,删除链表的倒数第 n 个结点,并且返回链表的头结点。
思想
首先后指针移动到第n个结点,然后前后指针同时后移,直到后指针的指针指向空,此时前指针指向倒数第n+1个结点,删去后一个结点即可。
ListNode* removeNthFromEnd(ListNode* head, int n) {
auto *h=new ListNode(-1);//辅助头结点,避免换头的判断
h->next=head;
auto f=h,s=h;
while(n--&&f->next) f=f->next;
while(f->next) f=f->next,s=s->next;
s->next=s->next->next;
//if(s->next) s->next->last=s; //如果为双链表的话多一步
return h->next;
}
4.合并两个有序链表
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
思想:
类似于归并排序的并阶段,将两个有序数组合并。在两个数组上各维持一个位置指针,比较大小,依次排列即可。
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {//迭代版
auto h = new ListNode(-1), r = h;//r为尾结点
while(l1&&l2){
if(l1->val < l2->val) r->next = l1,l1 = l1->next,r = r->next;//把l1连到尾部
else r->next = l2,l2 = l2->next,r = r->next;
}
if(!l1) r->next = l2;
if(!l2) r->next = l1;
return h->next;
}
ListNode* mergeTwoLists(ListNode* l1, ListNode* l2) {//递归版
if(!l1) return l2;//空时
if(!l2) return l1;
if(l1->val < l2->val){
l1->next = mergeTwoLists(l1->next, l2);
return l1;
}
else{
l2->next = mergeTwoLists(l1,l2->next);
return l2;
}
}
5.旋转链表
给你一个链表的头节点 head ,旋转链表,将链表每个节点向右移动 k 个位置。
思想:
首先统计链表结点的个数n,k对n取模。后指针移动到第k位,然后前后指针同时后移直到后指针的指针为空,此时前指针指向倒数第k+1个结点,将后指针的指针指向头结点,头指针指向前指针的后面一个结点,前指针的指针指向空。画图易懂。
注意:
不能在用到某个结点的指针前给这个结点的指针赋值。千万别犯这个错误。
ListNode* rotateRight(ListNode* head, int k) {
if(!head) return head;
auto f = head, s = head;
int n = 0;
while(f){n++,f = f->next;}
k = k%n;
f = head;
while(k-- && f->next) f = f->next;
while(f->next) s = s->next, f = f->next;
f->next = head;
head = s->next;
s->next = NULL;
return head;
}
6.反转链表
给你单链表的头指针 head 和两个整数 left 和 right ,其中 left <= right 。请你反转从位置 left 到位置 right 的链表节点,返回 反转后的链表 。
思想:
首先前指针指向新建的头结点,后指针指向原来的头结点。后指针后移n-m位到第n-m+1个结点,然后前后指针同时后移m-1位,后指针此时位于第n位,前指针位于第m-1位。然后执行以前指针的后一个结点为头,后指针为尾的翻转操作,执行完后将指针位置对应即可。
ListNode* reverseBetween(ListNode* head, int m, int n) {
if(m==n) return head;
auto h = new ListNode(-1);
h->next = head;
auto a = h,d = h->next;
for(int i = n-m;i > 0;i--) d = d->next;
while(m-->1){
a = a->next;
d = d->next;
}
auto c = a->next, b = d->next;
for(auto p = c, q = c->next; q!=b;){
auto r = q->next;
q->next = p;
p = q;
q = r;
}
a->next = d;
c->next = b;
return h->next;
}
7.有序链表转换二叉搜索树
给定一个单链表,其中的元素按升序排序,转换为高度平衡的二叉搜索树。
本题中,一个高度平衡二叉树是指一个二叉树每个节点 的左右两个子树的高度差的绝对值不超过 1。
思想:
万物皆可二分。无论树的结点是奇数个还是偶数个,最终的结果都是平衡的。这一建树思想广泛用于线段树。
vector<int> v;
int mid;
TreeNode* sortedListToBST(ListNode* h) {
v.clear();
while(h){
v.push_back(h->val);
h=h->next;
}
return buildTree(0,v.size()-1);
}
TreeNode* buildTree(int l,int r){
if(l>r) return NULL;
int mid=(l+r)/2;
TreeNode *p=new TreeNode(v[mid]);
p->left=buildTree(l,mid-1);
p->right=buildTree(mid+1,r);
return p;
}
8.环形链表
给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。
不允许修改 链表。
思想:
证明比较麻烦。记住快慢指针的流程即可。最终快指针比慢指针多跑的步数是圈数的整数倍。
ListNode *detectCycle(ListNode *head) {
ListNode *slow=head,*fast=head;
while(fast){
slow = slow->next;
fast = fast->next;
if(fast) fast = fast->next;
else break;
if(slow==fast){
slow = head;
while(slow!=fast){
slow = slow->next;
fast = fast->next;
}
return slow;
}
}
return NULL;
}
9.对链表进行插入排序
给定单个链表的头 head ,使用 插入排序 对链表进行排序,并返回 排序后链表的头 。
插入排序 算法的步骤:
- 插入排序是迭代的,每次只移动一个元素,直到所有元素可以形成一个有序的输出列表。
- 每次迭代中,插入排序只从输入数据中移除一个待排序的元素,找到它在序列中适当的位置,并将其插入。
- 重复直到所有输入数据插入完为止。
思想:
思想与数组插入排序一致。h作为链表的头结点,初始化为空,遍历以head为头结点的链表,将每一个结点插入h结点后对应的位置。
ListNode* insertionSortList(ListNode* head) {
auto h = new ListNode(-1),pre = h,q=head;
for(auto p = head; p; p=q){//把head的每个节点p插入到h链中
for(pre = h; pre->next&&p->val>=pre->next->val;pre = pre->next){}//找插入点
q = p->next,p->next = pre->next, pre->next = p;//插入
}
return h->next;
}
10.链表的归并排序
给你链表的头结点 head ,请将其按 升序 排列并返回 排序后的链表 。
思想:
思想与数组归并排序一致。首先将链表不断二分排序,直到满足边界条件,然后将有序的链表合并。同上。
ListNode *sortList(ListNode *head){
if (!head || !head->next) return head;
auto s = head, f = head->next;
//找到链表的中间位置
while (f&&f->next){ //快慢指针,注意必须前两步存在
s = s->next;
f = f->next->next;
}
auto l1 = sortList(s->next); //右链表
s->next = NULL; //将其断开,为两个链表
auto l2 = sortList(head);
return merge(l1, l2);
}
ListNode *merge(ListNode *l1, ListNode *l2){ //同上面的链表合并
auto h = new ListNode(-1), r = h;//r为尾结点
while(l1&&l2){
if(l1->val < l2->val) r->next = l1,l1 = l1->next,r = r->next;//把l1连到尾部
else r->next = l2,l2 = l2->next,r = r->next;
}
if(!l1) r->next = l2;
if(!l2) r->next = l1;
return h->next;
}
11.相交链表
给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null 。
思想:
链表的长度不一定相同,所以依次遍历找到相同结点是行不通的。注意到两个链表长度相交结点后的长度C相等,于是可以构造A+B-C=B+A-C,即同时遍历两个链表,遍历完后遍历另一个链表, 遍历到相同结点后终止,这样就能找到相交的起始结点。
ListNode *getIntersectionNode(ListNode *headA, ListNode *headB) {
auto pa = headA,pb = headB;
while(pa!=pb){
pa = (pa ? pa->next:headB);
pb = (pb ? pb->next:headA);
}
return pa;
}
12.删除排序链表中的重复元素
给定一个已排序的链表的头 head , 删除所有重复的元素,使每个元素只出现一次 。返回 已排序的链表 。
思想:
遍历链表,关注当前元素和下一个元素,如果相等,则删除下一个元素,否则当前元素指向下一个元素。
ListNode* deleteDuplicates(ListNode* h) {
auto f=h;
while(f){
if(f->next&&f->next->val==f->val) f->next=f->next->next;
else f=f->next;
}
return h;
}
13.删除排序链表中的重复元素II
给定一个已排序的链表的头 head , 删除原始链表中所有重复数字的节点,只留下不同的数字 。返回 已排序的链表 。
思想:
将不存在相同结点的结点插到头结点h后面,否则直接跳过。判别是否存在相同结点可以让与下一个结点值相同的结点后移,如果未后移则不存在相同结点,将结点加到尾节点t后。
ListNode* deleteDuplicates(ListNode* head) {
auto h=new ListNode(-1),t=h,p=head;
while(p){
auto q=p;
while(p->next&&p->next->val==p->val) p=p->next;//发现相同前后结点相同后移
if(q==p)//如果未后移则不存在相同结点,将结点加到尾节点t后
t->next=p,t=t->next,p=p->next;
else p=p->next; 否则跳过相同结点
}
t->next=NULL;尾指针赋为空
return h->next;
}
}