文章目录
学习链表之前,我们应该对指针有一定了解。
#include<stdio.h>
#include<stdlib.h>
int main()
{
int *p=(int*)malloc(sizeof(int));
*p=10;
printf("%d",*p);
}
链表的基本实现
链表有俩类节点,头节点和其他节点。
头结点只有数据域,其他节点包含数据域和指针域,最后一个节点的地址域为null.
struct node{
int data;
struct node*next;
}
创建链表:
int main(){
//head指向链表的最开始,链表还没有的时候head==null
struct node*head=NULL,*p,*q;
for(i=1;i<n;i++)
{//创建节点
struct node* p=(struct node*)malloc(sizeof(struct node));
p->data=a;
p->next=NULL;
//设置头指针的指向
if(head==NULL)
head=p;//如果这是创建的第一个结点,将头指针指向这个结点
else q->next=p;
//如果不是第一个创建的结点,则把上一个结点的后续指针指向当前结点
q=p;//指针q指向当前结点
}
}
在有序链表中插入一个比某个值大的数字。
Node* insert(int a,Node* head)
{
Node* t=head;
while(t!=NULL)
{
if(t->next->data>a)
//当前结点的下一个结点的值大于待插入的数字,将这个数字插入到中间
{
p=(struct node*)malloc(sizeof(node));
p->data=a;
//插入操作
p->next=t->next;
t->next=p;
break;
}
t=t->next;
}
return head;
}
数组模拟实现链表
t=1;
while(t!=0)
{
if(data[right[t]]>data[t])
//如果当前节点的下一个结点的值大于待插入的数,将数字插到中间
{
right[len]=right[t];
//新插入的下一个节点标号等于当前结点的下一个结点编号
right[t]=len;
//当前节点的下一个节点编号就是新插入数字的编号
break;
}
t=right[t];
}
链表的应用
(一)对系统中不连续的内存进行动态分配
(二)LRU缓存淘汰算法
CPU不能直接操纵地址,需要借助寄存器AX来存储,但是造价比较高。于是引入了L1,L2的cache缓存器,然后再到内存。
力扣刷题
c++中
#define NULL 0;
#define nullpter ((void*)0);
141 142 201环形链表
有一个链表的头节点 head ,判断链表中是否有环并且返回入环的点。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环, pos 来表示链表尾连接到链表中的位置。
思考一:(1)打张哈希表记录遍历得到的每个值。
如果一直遍历到某节点的next结点为null,则链表没有环,遍历结束。如果遍历到出现重复值的时候,说明链表有环。而当要寻找节点的时候,返回该节点就可以。
不足:(1)链表中不同节点有重复值的时候,该判断失误。
(2)哈希表的长度不确定(通过数组可以实现一个哈希表)
int k[100],count=0;
for(int i=0;i<10;i++)
{
i=k[count++];
}
(3)需要申请hashmap动态增长,消耗空间。
思考二 快慢指针,步长为一
(1)首次相遇时,慢和快指向同一个节点,S慢=S1+D S快=D+S1+n(S2+S1)----->此时快指针比慢指针多走了n圈。
(2)V快=2V慢---->S1+D=n(S2+S1)—>D=(n-1)环+S2
当相遇时,从头到入环的距离,等于首次相遇往后走(n-1)圈再走个S2
(3)此时忽略圈数则 D=S2
想法:引入慢指针other使得其与慢指针slow相遇,得到入环位置。
class Solution {
public:
bool hasCycle(ListNode *head) {
if(head==NULL) return false;
if(head->next=NULL) return false;
ListNode* slow=head,* fast=head;
//如果这个环只有一个元素,当slow=fast=head的时候,也会导致跳出循环.所以先进行一次循环
do
{
//循环跳出:直链表(快指针一下俩步)fast->next或者fast!=null
//slow==fast
fast=fast->next->next;
slow=slow->next;
} while(slow!=fast&&fast&&fast->next);
if(fast==slow) return true;
else return false;
//可以简化为return fast&&fast->next;
}
class Solution {
public:
ListNode *detectCycle(ListNode *head) {
if(head==nullptr) return nullptr;
if(head->next==nullptr) return nullptr;
ListNode* fast=head,*slow=head,*other=head;
do
{
fast=fast->next->next;
slow=slow->next;
}while(slow!=fast&&fast&&fast->next);
if(fast&&fast->next)
{//有环
while(fast!=other)
{
other=other->next;
fast=fast->next;
}
return fast;
}else{
return nullptr;
}
}
};
编写一个算法来判断一个数 n 是不是快乐数。
「快乐数」 定义为:
对于一个正整数,每一次将该数替换为它每个位置上的数字的平方和。
然后重复这个过程直到这个数变为 1,也可能是 无限循环 但始终变不到 1。
如果这个过程 结果为 1,那么这个数就是快乐数。
也就是说:判断是否有环,遍历结点如果有1就是快乐数,说明是个环,否则不是。
class Solution {
public:
int num1(int n)
{
int sum=0;
while(n>0)
{
int bit=n%10;
sum=sum+bit*bit;
n/=10;
}
return sum;
}
bool isHappy(int n) {
int slow=n,fast=n;
do
{
slow=num1(slow);
fast=num1(num1(fast));
}while(slow!=fast&&fast!=1);//无环或者是fast赶上1
return fast==1;
}
};
代码注意:(1)判断为空时对head和head->next均判断
(2)使用do while循环书写
(3)fast和slow均设置为起始node
206 92 25链表反转
思路一:入栈出栈,知道链表的长度。
不足:需要申请额外的空间。
思路二 双指针(记录断开结点的下一个结点 t=cur->next(),并且让**原链表的头结点去指向反转后列表的头结点 cur->next=pre **,后移pre=cur,然后与后面的结点连接cur=t,当cur==null循环停止)
(1)定义两个指针: pre和cur ;pre指向空,cur指针所指的节点指向pre所指向的节点,也是空。pre记录翻转后链表的头结点。cur为原链表的头结点。
(2)移动pre到cur,把cur移动到cur的next(保证右边的节点不会断)
(3)cur的next往后走
(4)循环上述过程,直至 cur指向空。
整体类似于打井,是一个迭代的过程。pre和cur起始都指向空,通过cur.next知道井的大小,然后往下打井,每次先移动cur,此时移动cur.next就可以继续知道后续井的大小,cur往下打井,cur指向pre,然后pre和cur继续向前移动。
class Solution {
public:
ListNode* reverseList(ListNode* head) {
ListNode* pre = NULL, *cur = head;
while (cur != NULL) {
ListNode* t = cur->next;//保存cur的后面一个指针的位置
cur->next = pre;//反转cur的指向
pre = cur;//更新pre的值后移
cur = t;//更新cur的值后移
}
return pre;
}
};
package com.kkb;
public class ExceptionDemo06 {
// 模拟数据库中已存在账号
private static String[] names = {"bill","hill","jill"};
public static void main(String[] args) {
//调用方法
try{
// 可能出现异常的代码
checkUsername("bill");
System.out.println("注册成功");//如果没有异常就是注册成功
}catch(LoginException e){
//处理异常
e.printStackTrace();
}
}
//判断当前注册账号是否存在
//因为是编译期异常,又想调用者去处理 所以声明该异常
public static boolean checkUsername(String uname) throws LoginException{
for (String name : names) {
if(name.equals(uname)){//如果名字在这里面 就抛出登陆异常
throw new LoginException("亲"+name+"已经被注册了!");
}
}
return true;
}
}
25.k个一组翻转链表
一个链表每 k 个节点一组进行翻转 返回翻转后的链表。
k 是一个正整数,它的值小于或等于链表的长度。
如果节点总数不是 k 的整数倍,那么请将最后剩余的节点保持原有顺序。
思考:(1)找到待翻转的k个结点。(双指针)(2)进行翻转,返回一个翻转后的头结点和尾结点(3)再对下一轮k个节点进行操作,将每一轮翻转的k个节点连接。
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode reverseKGroup(ListNode head, int k) {
ListNode hair(-1,head);
ListNode pre=head,tail=null;
while(pre!=null)
{
for(int i=0;i<left;i++)
{
tail=tail.next();
if(tail==null)
{
return hair.next;
}
}//找到k个一组的翻转区域
ListNode[] re=reverse1(head,tail);
head=re[0];
tail=re[1];//用数组接收翻转后的头结点和尾结点
pre.next=head;
pre=tail;//pre向后移动。pre就是下面一个待搬运节点的头结点
head=pre.next;
}
}
public ListNode reverse1(ListNode head,ListNode tail)
{
ListNode pre=head.next,cur=head;//pre指向head的下一个节点
while(cur!=tail)
{
ListNode t=cur.next;
cur.next=pre;
pre=cur;
cur=t;
}
return ListNode[]{tail,head};
}
}
61旋转链表
倒数第k个结点和倒数第k+1个结点断开。
把倒数第k个结点连接到倒数第二个结点的头部。
思考链表成环,找到倒数第k个结点和倒数第k+1个结点断开,返回倒数第k个结点。
当k>length的时候,k%len=1,减少旋转的次数。
做法(1)将链表成环。
(2)找到倒数第k个结点和倒数第k+1个结点。
(3)将倒数第k个结点和倒数第k+1个结点断开。
(4)返回倒数第k个结点。
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode rotateRight(ListNode head, int k) {
if(head==null||head.next==null)
return head;
int len=1;
ListNode tail=head;
while(tail.next!=null)
{
tail=tail.next;
len++;
}//算链表长度
tail.next=head;//成环
ListNode newtail=head;//找到倒数第k+1结点(新链表尾部)
for(int i=0;i<len-k%len-1;i++)
{
newtail=newtail.next;
}
ListNode newhead=newtail.next;//新链表的头部(倒数第k个结点)
newtail.next=null;//断开环
return newhead;
}
}
24.俩俩交换链表中的结点
**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode swapPairs(ListNode head) {
ListNode hair=new ListNode(-1,head);
ListNode pre=hair;
while(pre.next!=null&&pre.next.next!=null)
{
//判断还剩一个或者俩个结点
ListNode one=pre.next;
ListNode two=pre.next.next;
one.next=two.next;
two.next=one;
pre.next=two;//前俩组结点翻转完成
pre=one;//移动pre往后走
}
return hair.next;
}
}
91 82 83链表的删除
删除第N个结点,也就是找到倒数第N+1个结点。
(1)定义俩个指针,一个指针指向第N个结点(先指向头结点,向后走N步),另一个指针从虚拟头结点开始。
(2)俩个指针同时向前移动,当第一个指针指向空的时候,就找到第N+1个结点。
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode removeNthFromEnd(ListNode head, int n) {
ListNode hair=new ListNode(-1,head);
ListNode p=head,q=hair;
while(n-->0)
{
p=p.next;
}
while(p!=null)
{
p=p.next;
q=q.next;
}
q.next=q.next.next;
//删除第n个结点
return hair.next;
}
}
删除有序链表的相同元素
(1)相同值必定相邻,可以用cur指向第一个结点。
(2)如果cur后面的值和cur相同,移动cur.next=cur.next.next
(3) 如果不相同,继续移动cur
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode deleteDuplicates(ListNode head) {
if(head==null) return null;
ListNode cur=head;
while(cur!=null &&cur.next!=null)
{
if(cur.val==cur.next.val)
{
cur.next=cur.next.next;
}else{
cur=cur.next;
}
}
return head;
}
}
给定一个已排序的链表的头 head , 删除原始链表中所有重复数字的节点,只留下不同的数字 。返回 已排序的链表 。
(1)pre指向虚拟头结点,cur从头结点后移。如果cur.next和cur相等,pre和cur继续移动,直到不相等的时候。
(2)删除pre到cur.next的所有结点
(3)pre和cur各向前走一步
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode deleteDuplicates(ListNode head) {
if(head==null) return null;
ListNode hair=new ListNode(-1,head);
ListNode pre=hair,cur=head;
while(cur!=null&&cur.next!=null)
{
if(cur.val!=cur.next.val)
{
pre=pre.next;
cur=cur.next;
}
else{
while(cur!=null&&cur.next!=null&&cur.val==cur.next.val)
{
cur=cur.next;
}
pre.next=cur.next;
cur=cur.next;
}
}
return hair.next;
}
}
86分割链表
给你一个链表的头节点 head 和一个特定值 x ,请你对链表进行分隔,使得所有 小于 x 的节点都出现在 大于或等于 x 的节点之前。你应当 保留 两个分区中每个节点的初始相对位置。(实现稳定排序)
根据比较x和p的值的大小分成俩个链表。
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode() {}
* ListNode(int val) { this.val = val; }
* ListNode(int val, ListNode next) { this.val = val; this.next = next; }
* }
*/
class Solution {
public ListNode partition(ListNode head, int x) {
ListNode r1,r2,*p1=&r1,*p2=&r2;
ListNode *p=head,*q;
while(p)
{
q=p->next;
//big small
if(p->val<x)
{
p->next=p1->next;
p1->next=p;//插入p,先往后面指,再改变指向
p1=p;
//插入p再移动
}else
{
p->next=p2->next;
p2->next=p;
p2=p;
}
p=q;
}
p1->next=r2.next;//连接
return r1.next;
}
}
138复制带随机指针的链表
给你一个长度为 n 的链表,每个节点包含一个额外增加的随机指针 random ,该指针可以指向链表中的任何节点或空节点。
构造这个链表的 深拷贝。 深拷贝应该正好由 n 个全新节点组成,其中每个新节点的值都设为其对应的原节点的值。新节点的 next 指针和 random 指针也都应指向复制链表中的新节点,并使原链表和复制链表中的这些指针能够表示相同的链表状态。复制链表中的指针都不应指向原链表中的节点 。
例如,如果原链表中有 X 和 Y 两个节点,其中 X.random --> Y 。那么在复制链表中对应的两个节点 x 和 y ,同样有 x.random --> y 。
class Solution {
public Node copyRandomList(Node head) {
if(head==nullptr)
{
return nullptr;
}
Node*p=head;
Node* new_head;
while(p)
{
//copy node
Node *q=new Node(p->val);//定义一个新节点
q->random=p->random;
q->next=p->next;
p->next=q;
//插入结点
p=q->next;
//往后移动,继续插入下一个
}
//回到头结点
p=head;
while(p)
{
if(p->random) q->random=p->random->next;
(p=p->next) &&(p=p->next);
//p=p->next;
//if(p!=nullptr)
// p=p->next;
}
//创建新链表,每次往后指,都是每次走俩步
new_head=head.next;
while(p)
{
Node* q=p->next;
p->next=q->next;
if(p->next) q->next=p->next->next;
p=p->next;
}
return new_head;
}
}
链表使用总结
1)对于笔试,不必在乎空间复杂度,一切为了时间。
2)面试的时间复杂度放第一位,但一定要找到空间最优的方法。
技巧:
额外数据结构记录(哈希表等)+快慢指针(快指针一次走俩步,慢指针一次走一步,快指针来到终点位置的时候,慢指针刚好走到中间,从而把慢指针后面的放进栈里面
判断一个链表是否为回文单链表
方法(一):栈存放所有元素,往出弹出的顺序就是逆序的顺序,每弹出一个都和原来的顺序比对。
方法(二):把对称轴右边的放在栈里面,每遇到一个往出弹一个和左边比较,直到栈弹空了。
方法 (三):快指针走俩步,慢的走一步,快指针走完后,慢指针来中点。然后让中点往下遍历的时候逆序,中间指向空,右边的往回指。快和慢指向的每个位置都进行比对。
例:将单链表化成左边大,中间相等,右边大的形式
方法(一):
直接将链表中的值保存到一个数组中,然后按照荷兰国旗的划分方式,将数组划分成左边小于那个数,中间等于那个数,右边大于那个数的形式,(荷兰国旗问题用于快速排序中的partition过程)
划分完之后,再把数组中的值用链表的形式连接起来
static class Node {
int value;
Node next;
Node(int value) {
this.value = value;
}
}
//普通的需要额外空间O(n)且不能达到稳定性的方法
static Node partitionList_1(Node head, int mid) {
if (head == null) return null;
Node cur = head;
int len = 0;
while (cur != null) {
len++;
cur = cur.next;
}
Node nodeArr[] = new Node[len];
cur = head;
for (int i = 0; i < nodeArr.length; i++) {
nodeArr[i] = cur;
cur = cur.next;
}
arrPartition(nodeArr, pivot);
for (int i = 1; i < nodeArr.length; i++) {
nodeArr[i - 1].next = nodeArr[i];
}
nodeArr[nodeArr.length - 1].next = null; //一定要记得把最后一个指针指向null
return nodeArr[0];
}
//数组划分的paration
void arrPartition(Node[] nodeArr, int pivot) {
int less = -1;
int more = nodeArr.length;
int cur = 0;
while (cur < more) {
if (nodeArr[cur].value < pivot) {
swap(nodeArr, ++less, cur++);
} else if (nodeArr[cur].value > pivot) {
swap(nodeArr, --more, cur); //注意放到大于区域的时候cur不能++
} else {
cur++;
}
}
}
//交换两个结点
void swap(Node[] arrNode, int a, int b) {
Node temp = arrNode[a];
arrNode[a] = arrNode[b];
arrNode[b] = temp;
}
[点击并拖拽以移动]
方法(二):
这个方法是将原来的链表依次划分成三个链表,三个链表分别为small代表的是左边小于的部分,equal代表的是中间相等的部分,big代表的是右边的大于部分;
这三个链表都有自己的两个指针H和T分别代表各自的头部和尾部,分成三个子链表之后,我们只需要遍历链表,然后和给定的值比较,按照条件,向三个链表中添加值就可以了,最后把三个链表连接起来就可以了
Node partitionList_2(Node head,int piovt){
if(head == null)return null;
Node sH = null,sT = null; //小于部分链表的 head 和tail
Node eH = null,eT = null; //等于部分链表的 head 和tail
Node bH = null,bT = null; //大于部分链表的 head 和tail
Node next = null; //用来保存下一个结点
//划分到 三个不同的链表
while(head != null){
next = head.next;
head.next = null; //为了链表拼接后最后一个就不用再去赋值其next域为null 了
if(head.value < piovt){
if(sH == null){ //small部分的第一个结点
sH = head;
sT = head;
}else {
sT.next = head; //把head放到small最后一个
sT = head; //更新small部分的sT
}
}else if(head.value == piovt){
if(eH == null){
eH = head;
eT = head;
}else{
eT.next = head;
eT = head;
}
}else {
if(bH == null){
bH = head;
bT = head;
}else {
bT.next = head;
bT = head;
}
}
head = next;
}
//将三个链表合并(注意边界的判断)
if(null != sT) { //合并small和equal部分
sT.next = eH;
eT = (eT == null) ? sT : eT;
}
if(null != eT){
eT.next = bH;
}
return sH != null ? sH : eH != null ? eH : bH;
}