链表小总结

1. 介绍:

1.分类:单链表、双链表、循环链表(可以用来解决约瑟夫环问题)。

2.存储方式:链表的节点在内存中是分散存储的,通过指针连在一起。

3.时间复杂度:链表的增添和删除都是O(1);查找的时间复杂度是O(n)。

2.链表的定义: 

public class ListNode {
    // 结点的值
    int val;

    // 下一个结点
    ListNode next;

    // 节点的构造函数(无参)
    public ListNode() {
    }

    // 节点的构造函数(有一个参数)
    public ListNode(int val) {
        this.val = val;
    }

    // 节点的构造函数(有两个参数)
    public ListNode(int val, ListNode next) {
        this.val = val;
        this.next = next;
    }
}

3.哨兵结点/双指针

160.相交链表

双指针:

1.首先排除链表 headA 和 headB为空的情况;

2.创建两个指针 pA和 pB,初始时分别指向两个链表的头节点 headA和 headB,然后将两个指针依次遍历两个链表的每个节点。

3.当指针 pA 为空,则将指针 pA\ 移到链表 headB 的头节点;如果指针 pB为空,则将指针 pB移到链表 headA\的头节点。

4.当指针 pA 和 pB指向同一个节点或者都为空时,返回它们指向的节点或者 null。

public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        if(headA == null || headB == null){
            return null;
        }
        ListNode pA = headA;
        ListNode pB = headB;
        while(pA != pB){
            pA = pA == null ? headB : pA.next;
            pB = pB == null ? headA : pB.next;
            }
        return pA;
    }
  • 时间复杂度:O(m+n),其中 m 和 n 是分别是链表 headA 和 headB 的长度。两个指针同时遍历两个链表,每个指针遍历两个链表各一次。
  • 空间复杂度:O(1)。

206.反转链表 

迭代法/双指针:

遍历链表时,将当前节点的 next指针改为指向前一个节点。由于节点没有引用其前一个节点,因此必须事先存储其前一个节点。在更改引用之前,还需要存储后一个节点。最后返回新的头引用。

public ListNode reverseList(ListNode head) {
        ListNode pre = null;
        ListNode cur = head;
        while(cur != null){
            ListNode next=cur.next;
            cur.next=pre;
            pre = cur;
            cur=next;
        }
        return pre;
        }
  • 时间复杂度:O(n),其中 n是链表的长度。需要遍历链表一次。

  • 空间复杂度:O(1)。

234. 回文链表

 方法一:将值复制到数组列表中,然后用双指针法。时间复杂度:O(n)   空间复杂度:O(n)

方法二:快慢指针

  1. 找到前半部分链表的尾节点。慢指针一次走一步,快指针一次走两步,快慢指针同时出发遍历。若链表有奇数个节点,则中间的节点应该看作是前半部分的,慢指针再后移一步。
  2. 反转后半部分链表。使用上一题的代码。
  3. 判断是否回文。比较前后两部分的值,当后半部分到达末尾则比较完成,可以忽略计数情况中的中间节点。
  4. 恢复链表。再反转一次。
  5. 返回结果。

下列代码位恢复链表。要恢复,需要找到中间结点的前一个结点。因此while循环里,改为fast.next != null && fast.next.next !=null,然后反转函数传参变成slow.next。

public boolean isPalindrome(ListNode head) {
        if(head == null){
            return true;
        }
        ListNode fast = head;
        ListNode slow = head;
        //奇数个数时,fast.next==null
        //偶数个数时,fast==null
        while(fast != null && fast.next != null){  
            fast=fast.next.next;
            slow=slow.next;
        }  
        
        //奇数情况
        if(fast != null){
            slow=slow.next;
        }
       
        
        ListNode newHead = reverseList(slow);
       
        while(newHead != null ){
            if(head.val != newHead.val){
                return false;
            }
            head = head.next;
            newHead=newHead.next;
        }
        
        return true;
    }

    private ListNode reverseList(ListNode head){
        ListNode cur = head;
        ListNode pre = null;
        while(cur != null){
            ListNode next = cur.next;
            cur.next = pre;
            pre = cur;
            cur =next;
        }
        return pre;
    }
  • 时间复杂度:O(n)

  • 空间复杂度:O(1)

141.环形链表

 快慢指针(龟兔赛跑算法):

定义两个指针,一快一慢。慢指针每次只移动一步,而快指针每次移动两步。

初始时,慢指针在位置 head,而快指针在位置 head.next。

在移动的过程中,当快指针反过来追上慢指针,就说明该链表为环形链表。否则快指针将到达链表尾部,该链表不为环形链表。

注意:快慢指针的初始化与「乌龟」和「兔子」中的叙述不同(都从head开始):

这是为了保证循环的执行,否则,循环一开始就不会执行。

public boolean hasCycle(ListNode head) {
        if(head == null || head.next == null){
            return false;
        }
        ListNode fast = head.next;
        ListNode slow = head;
        while(fast!=slow){
            if(fast == null || fast.next == null){
                return false;
            }
            fast=fast.next.next;
            slow = slow.next;
        }
        return true;
    }
  • 时间复杂度:O(N)
  • 空间复杂度:O(1)

142.环形链表Ⅱ

 快慢指针:

与上一题一样,只不过循环变成fast !=null && fast.next!=null。先在slow与fast相遇的时候,一定时在环上相遇。然后初始化一个指针从相遇点一步一步往前走,同时head也一步一步往前,当两者相遇时,就是环的入口。

public ListNode detectCycle(ListNode head) {
        if(head == null || head.next==null){
            return null;
        }
        ListNode fast = head;
        ListNode slow = head;

        while(fast != null && fast.next !=null){
            slow=slow.next;
            fast=fast.next.next;
            //有环
            if(slow == fast){
                ListNode p=fast;
                while(p != head){
                    head=head.next;
                    p=p.next;
                }
                return p;
            }

        }
        return null;
        
    }
  • 时间复杂度:O(N),

    slow 指针走过的距离不会超过链表的总长度;随后寻找入环点时,走过的距离也不会超过链表的总长度。因此,总的执行时间为 O(N)+O(N)=O(N)

  • 空间复杂度:O(1)

21.合并两个有序链表

迭代(哨兵结点):

1.设定一个哨兵节点 preHead ,便于返回合并后的链表。

2.维护一个 prev 指针,调整它的 next 指针。

3.循环将更小的值添加到pre后面,并把 pre 向后移一位。直到有一个链表的指针指向了 null 。

4.在循环终止的时候,至多有一个链表是非空的。我们只需要简单地将非空链表接在合并链表的后面,并返回合并链表即可。

public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
        if(list1 == null){
            return list2;
        }else if(list2==null){
            return list1;
        }  
        //设定一个哨兵结点,也就是虚拟头结点;     
        ListNode preHead = new ListNode(-1);
        ListNode pre = preHead; 

        while(list1 != null && list2 !=null){
            if(list1.val > list2.val){
                pre.next = list2;
                list2=list2.next;
            }else {
                pre.next= list1;
                list1=list1.next;
            }
            pre =pre.next;            
        }
        pre.next = list1==null ? list2 : list1;
        return preHead.next;
    }
  • 时间复杂度:O(n+m)   
  • 空间复杂度:O(1)。

2.两数相加 

由于输入的两个链表都是逆序存储数字的位数的,因此两个链表中同一位置的数字可以直接相加。

1.同时遍历两个链表,逐位计算它们的和,并与当前位置的进位值相加。

2.如果当前两个链表处相应位置的数字为 n1,n2,进位值为 carry,则它们的和为 n1+n2+carry;其中,答案链表处相应位置的数字为 (n1+n2+carry) mod 10,而新的进位值为 (n1+n2+carry)/10。

3.如果链表遍历结束后,有 carry>0,还需要在答案链表的后面附加一个节点,节点的值为 carry。

注意:其中涉及到很多判断!!!

public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
        ListNode head = null,tail=null;
        int carry=0;
        while(l1!=null || l2 !=null){
            int n1 = l1 != null ? l1.val : 0;
            int n2 = l2 !=null ? l2.val:0;
            int sum = n1+n2+carry;
            if(head == null){
                head = tail = new ListNode(sum%10);
            }else{
                tail.next = new ListNode(sum%10);
                tail=tail.next;
            }
            carry=sum /10;
            if(l1!=null){
                l1=l1.next;
            }
            if(l2!=null){
                l2=l2.next;
            }
        }
        if(carry > 0){
            tail.next=new ListNode(carry);
        }
        return head;
}

19.删除链表的倒数第N个节点 

双指针(哨兵结点)

要删除倒数第n个节点,我们要得到的是倒数第 n 个节点的前驱节点而不是倒数第 n个节点。

我们可以使用两个指针fast和 slow同时对链表进行遍历,并且 fast比 slow超前 n+1 个节点。当 first 遍历到链表的末尾时,slow就恰好处于倒数第 n 个节点的前一个。

具体地,初始时fast指向头节点,slow指向哑节点,。我们首先使用 fast对链表进行遍历,遍历的次数为 n。此时,fast比 slow 超前了 n+1 个节点。

在这之后,我们同时使用fast和 slow 对链表进行遍历。当 fast遍历到链表的末尾(即fast为空指针)时,slow 恰好指向倒数第 n个节点。

public ListNode removeNthFromEnd(ListNode head, int n) {
        //哨兵结点
        ListNode dummy=new ListNode(0,head); 
        ListNode slow=dummy;
        ListNode fast=head;
        for(int i=0; i<n; i++){
            fast = fast.next;
        }
        while(fast != null){
            fast=fast.next;
            slow = slow.next;
        }
        slow.next = slow.next.next;
        return dummy.next;
    }
  • 时间复杂度:O(n)

  • 空间复杂度:O(1)

 24.两两交换链表中的节点

迭代:哨兵结点

public ListNode swapPairs(ListNode head) {
        ListNode dummy = new ListNode(0,head);
        ListNode pre = dummy;

        while(pre.next != null && pre.next.next !=null){
            // 记录第一个结点
            ListNode n1 = pre.next;
            // 记录第二个结点
            ListNode n2 =pre.next.next;
            // 指针指向第二个结点
            pre.next = n2;
            // 第一个结点指向第二个节点的后面
            n1.next = n2.next;
            // 第二个结点指向第一个结点
            n2.next=n1;
            // 移动指针至下一个位置
            pre =n1;
        }
        return dummy.next;
    }
  • 时间复杂度:O(n)

  • 空间复杂度:O(1)

 138.复制带随机指针的链表

 本题难点: 在复制链表的过程中构建新链表各节点的 random 引用指向。遍历复制,无法复制到random指针。

迭代 + 节点拆分:

考虑构建 原节点 1 -> 新节点 1 -> 原节点 2 -> 新节点 2 -> …… 的拼接链表,如此便可在访问原节点的 random 指向节点的同时找到新对应新节点的 random 指向节点。

需要注意原节点的随机指针可能为空,我们需要特别判断这种情况。

当我们完成了拷贝节点的随机指针的赋值,我们只需要将这个链表按照原节点与拷贝节点的种类进行拆分即可,只需要遍历一次。同样需要注意原链表的最后一个节点的后继节点为空,我们需要特别判断这种情况。

 public Node copyRandomList(Node head) {
        if(head == null){
            return null;
        }
        // 1. 复制各节点,并构建拼接链表
        Node cur = head;
        while(cur != null){
            Node temp = new Node(cur.val);
            temp.next = cur.next;
            cur.next = temp;
            cur=temp.next;
        }
        // 2. 构建各新节点的 random 指向
        cur = head;
        while(cur != null){
            if(cur.random != null){
                cur.next.random=cur.random.next;
            }
            cur = cur.next.next;
        }
       // 3. 拆分两链表
       cur = head.next;
       Node pre = head, res=head.next;
       while(cur.next !=null){
        pre.next = pre.next.next;
        cur.next = cur.next.next;
        cur = cur.next;
        pre=pre.next;
       }
        pre.next=null;// 单独处理原链表尾节点
        return res;// 返回新链表头节点
    }
  • 时间复杂度 O(N)
  • 空间复杂度 O(1)

148. 排序链表 

时间复杂度是 O(nlog⁡n)的排序算法包括归并排序、堆排序和快速排序(快速排序的最差时间复杂度是 O(n^2),其中最适合链表的排序算法是归并排序。

归并排序基于分治算法。最容易想到的实现方式是自顶向下的递归实现,考虑到递归调用的栈空间,自顶向下归并排序的空间复杂度是 O(log⁡n)。如果要达到 O(1)的空间复杂度,则需要使用自底向上的实现方式。

public ListNode sortList(ListNode head) {
        if(head == null){
            return null;
        }
        // 遍历得到链表的长度
        int len = 0;
        ListNode cur = head;
        while(cur != null){
            len++;
            cur = cur.next;
        }
        // 哨兵结点
        ListNode dummy = new ListNode(-1,head);
        //将链表拆成若干长度位subLen的子链表,两两一组进行归并排序,直至subLen=len
        // subLen加倍增加 subLen *= 2 
       for (int subLen = 1; subLen < len; subLen <<= 1) {
            // 记录头结点
            ListNode prev = dummy;
            cur = dummy.next;
            while (cur != null) {
                //记录第一个分组的头节点
                ListNode head1 = cur;
                //找到第一个分组的尾节点
                for (int i = 1; i < subLen && cur.next != null; i++) {
                    cur = cur.next;
                }
                //记录下一个分组的头节点
                ListNode head2 = cur.next;
                //重置cur,并让其指向第二个分组的头结点
                cur.next = null;
                cur = head2;
                //找到下一个分组的尾结点
                for (int i = 1; i < subLen && cur != null && cur.next != null; i++) {
                    cur = cur.next;
                }
                // 记录下两个分组的开始
                ListNode next = null;
                if (cur != null) {
                    next = cur.next;
                    cur.next = null;
                }
                // 合并两个组
                ListNode merged = merge(head1, head2);
                // 记录两个组合并的头结点
                prev.next = merged;
                while (prev.next != null) {
                    prev = prev.next;
                }
                // 开始进入下个循环处理接下来的两组数据
                cur = next;
            }
        }
        return dummy.next;
}
    public ListNode merge(ListNode list1,ListNode list2){
        ListNode preHead = new ListNode(-1);
        ListNode pre = preHead; 

        while(list1 != null && list2 !=null){
            if(list1.val > list2.val){
                pre.next = list2;
                list2=list2.next;
            }else {
                pre.next= list1;
                list1=list1.next;
            }
            pre =pre.next;            
        }
        pre.next = list1==null ? list2 : list1;
        return preHead.next;
    }
  • 时间复杂度:O(nlog⁡n)。

  • 空间复杂度:O(1)。

  • 34
    点赞
  • 32
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
链表是一种常用的数据结构,用于存储一系列元素。C语言中,链表是通过指针来实现的,每个节点包含数据和指向下一个节点的指针。 以下是链表的基础知识总结: 1. 链表的定义: ```c struct Node { int data; struct Node* next; }; ``` 其中,data 表示节点存储的数据,next 表示指向下一个节点的指针。 2. 链表的操作: - 创建节点: ```c struct Node* createNode(int data) { struct Node* node = (struct Node*) malloc(sizeof(struct Node)); node->data = data; node->next = NULL; return node; } ``` - 插入节点: ```c void insertNode(struct Node* head, int data) { struct Node* node = createNode(data); node->next = head->next; head->next = node; } ``` 其中,head 表示链表头节点。 - 删除节点: ```c void deleteNode(struct Node* head, int data) { struct Node* p = head->next; struct Node* q = head; while (p != NULL) { if (p->data == data) { q->next = p->next; free(p); break; } q = p; p = p->next; } } ``` - 遍历链表: ```c void traverseList(struct Node* head) { struct Node* p = head->next; while (p != NULL) { printf("%d ", p->data); p = p->next; } printf("\n"); } ``` - 销毁链表: ```c void destroyList(struct Node* head) { struct Node* p = head->next; while (p != NULL) { struct Node* q = p; p = p->next; free(q); } head->next = NULL; } ``` 3. 链表的优缺点: 链表的优点是插入和删除操作的时间复杂度为 O(1),而数组的时间复杂度为 O(n)。但是,链表的缺点是无法随机访问元素,需要遍历整个链表才能找到要查找的元素。此外,链表需要额外的空间来存储指向下一个节点的指针。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值