链表的常见操作


发现面试链表问的真的多。有时候面试官只让说思路,所以拿笔画一画,要能把思路捋清。

链表数据结构

class ListNode{
	int val;//节点有个值
	ListNode next;//节点有下一个节点
	ListNode(int val){//构造函数
		this.val = val;
	}
}

反转链表

思路:首先定义一个前驱节点为空,然后用一个当前指针cur指向当前节点,循环的调换方向。循环体四小步:(1)临时变量next保存当前节点cur的下一个节点;(2)当前节点cur的下一个节点指向前驱节点pre;(3)当前节点cur赋给前驱节点pre;(4)临时变量next赋给当前节点cur。

class Solution {
    public ListNode reverseList(ListNode head) {
        ListNode pre = null;
        ListNode cur = head;
        while(cur != null){
            ListNode next = cur.next;//临时变量next保存当前节点cur的下一个节点
            cur.next = pre;//当前节点cur的下一个节点指向前驱节点pre
            pre = cur;//当前节点cur赋给前驱节点pre
            cur = next;//临时变量next赋给当前节点cur
        }
        return pre;
    }
}

LRU实现

NC93 设计LRU缓存结构
146. LRU 缓存机制
有点难!但是最近面试官好像都喜欢考。
思路:这个问题用双向链表辅助哈希表来实现。

  1. 首先自己得能写出来一个双向链表的结构。
  2. 分别定义头尾节点,并互相连接
  3. 遍历提供的操作列表,先判断是什么操作
  4. 若插入,先看看哈希表里有键值冲突:没有的话,用键值新构造一个节点,插入到链表中,并移到链表头,还要size++,再看看有没有越界,越了的话要把尾部节点删除;同时放入哈希表中。有的话,用新值代替旧值,然后移到链表头。
  5. 若读取,判断键对应的节点存不存在,存在的话读值并把节点移到头部,不存在-1.
public class Solution {
    /**
     * lru design
     * @param operators int整型二维数组 the ops
     * @param k int整型 the k
     * @return int整型一维数组
     */
    class DoubleListNode {
        int key;
        int val;
        DoubleListNode pre;
        DoubleListNode next;
        DoubleListNode() {}
        DoubleListNode(int key, int val) {
            this.key = key;
            this.val = val;
        }
    }
    
    DoubleListNode head = new DoubleListNode(-1, -1);
    DoubleListNode tail = new DoubleListNode(-1, -1);
    HashMap<Integer, DoubleListNode> map = new HashMap<>();

    public int[] LRU (int[][] operators, int k) {
        // write code here
        ArrayList<Integer> ans = new ArrayList<>();
        head.next = tail;
        tail.pre = head;
        int size = 0;
        
        for(int i = 0; i < operators.length; i++){
            int tmp_key = operators[i][1];
            DoubleListNode node = map.get(tmp_key);
            if(operators[i][0] == 1){
                if(node == null){
                    DoubleListNode new_node = new DoubleListNode(tmp_key, operators[i][2]);
                    map.put(tmp_key, new_node);
                    add2head(new_node);
                    size++;
                    if(size > k){
                        DoubleListNode weiba = removeTail();
                        map.remove(weiba.key);
                        size--;
                    }
                }else{
                    node.val = operators[i][2];
                    add2head(node);
                }
            }else{
//                 node = map.get(tmp_key);
                if(node == null) ans.add(-1);
                else{
                    ans.add(node.val);
                    move2head(node);
                }
            }
        }
        int[] res = new int[ans.size()];
        for(int i = 0; i < ans.size(); i++){
            res[i] = ans.get(i);
        }
        return res;
    }
    
    void move2head(DoubleListNode node){
        removeNode(node);
        add2head(node);
    }

    void removeNode(DoubleListNode node){
        node.pre.next = node.next;
        node.next.pre = node.pre;
    }

    void add2head(DoubleListNode node){
        node.pre = head;
        node.next = head.next;
        head.next.pre = node;
        head.next = node;
    }

    DoubleListNode removeTail(){
        DoubleListNode weiba = tail.pre;
        removeNode(tail.pre);
        return weiba;
    }
}

判断链表有环

141. 判断是否有环
思路:快慢指针,只要快指针和快指针的next均不为空,就循环的让两个指针后移。一旦发现两个指针相等了,说明有环。否则循环结束必然是遇到了空,没环。

public class Solution {
    public boolean hasCycle(ListNode head) {
        ListNode fast = head;
        ListNode slow = head;
        while(fast != null && fast.next != null){
            fast = fast.next.next;
            slow = slow.next;
            if(fast == slow) return true;
        }
        return false;
    }
}

142. 环形链表 II-找到入环节点
思路一:直接用hashset不重复的特点,遍历链表,什么时候add失败了或者contains方法true了,说明遇到了。

public class Solution {
    public ListNode detectCycle(ListNode head) {
        HashSet<ListNode> set = new HashSet<>();
        ListNode cur = head;
        while(cur != null){
            if(set.contains(cur)){// or "!set.add(cur)"
                return cur;
            }
            set.add(cur);
            cur = cur.next;
        }
        return null;
    }
}

思路2:快慢指针,仔细听,首先fast和slow分别从head出发,fast跨两步,slow跨一步,等什么时候相遇了,fast走过了slow两倍的距离。
在这里插入图片描述
也就是图中x = z。那么这个时候fast回到head,slow在原位,他们同时一步一步走,正好在入口相遇。

public class Solution {
    public ListNode detectCycle(ListNode head) {
        ListNode fast = head;
        ListNode slow = head;
        while(fast != null && fast.next != null){
            fast = fast.next.next;
            slow = slow.next;
            if(fast == slow){
                fast = head;
                while(fast != slow){
                    fast = fast.next;
                    slow = slow.next;
                }
                return fast;
            }
        }
        return null;
    }
}

找出两个链表的交点

160. 相交链表
思路:两条链表长度不一致,但是如果两个指针,pA先走A链表,再走B链表;pB先走B链表,再走A链表,那么等他们什么时候相遇了,便是链表相交的地方。

public class Solution {
    public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
        ListNode pA = headA;
        ListNode pB = headB;
        while(pA != pB){
            if(pA == null) pA = headB;
            else pA = pA.next;
            if(pB == null) pB = headA;
            else pB = pB.next;
        }
        return pA;
    }
}

删除链表的倒数第 N 个结点

找倒数那道题就pass了。
19. 删除链表的倒数第 N 个结点
思路还是快慢指针了,快指针先往后走n步,然后快慢指针同时往后走,等fast走到头了,slow也就到我们要找的那个节点了。因为要删除,还要另外用个指针指向前驱结点。为了应对删除头节点的情况,前边加个哑结点辅助返回。

class Solution {
    public ListNode removeNthFromEnd(ListNode head, int n) {
        ListNode pre = new ListNode(-1);
        ListNode preNode = pre;
        pre.next = head;
        ListNode fast = head;
        ListNode slow = head;
        for(int i = 0; i < n; i++){
            fast = fast.next;
        }
        while(fast != null){
            fast = fast.next;
            slow = slow.next;
            pre = pre.next;
        }
        pre.next = slow.next;
        return preNode.next;
    }
}

K 个一组翻转链表

25. K 个一组翻转链表
思路:这个有点难。

  1. 定一个哑结点指向链表,辅助返回
  2. 循环的找k个节点构成一组,如果凑不足k个了,就返回
  3. 凑足了,定一个临时节点保存下一组的头结点(当前组的尾节点的next)
  4. 翻转当前组,翻转的时候尾巴会成功指向下一组的头
  5. 翻转操作类似反转链表,但要传入头尾节点,返回翻转后的头尾结点
  6. 翻转完了 前驱结点指向当前新的组
  7. 更新pre和head
class Solution {
    public ListNode reverseKGroup(ListNode head, int k) {
        ListNode hair = new ListNode(0);
        ListNode pre = hair;
        hair.next = head;

        while(head != null){
            ListNode tail = pre;
            for(int i = 0; i < k; i++){
                tail = tail.next;
                if(tail == null){
                    return hair.next;
                }
            }//此时 head指向第k组的头部节点,tail指向尾部节点
            
            ListNode nex = tail.next;
            ListNode[] reverse = reverseAGroup(head, tail);
            head = reverse[0];
            tail = reverse[1];
            pre.next = head;
            //tail.next = nex;
            pre = tail;
            head = tail.next;
        }
        return hair.next;
    }

    public ListNode[] reverseAGroup(ListNode head, ListNode tail){
        ListNode pre = tail.next;
        ListNode cur = head;
        while(tail != pre){
            ListNode nex = cur.next;
            cur.next = pre;
            pre = cur;
            cur = nex;
        }
        return new ListNode[]{tail, head};
    }
}

排序链表

148.排序链表
对链表进行排序,要求时间复杂度O(nlogn),空间复杂度O(1)。
使用归并排序。建议对比着数组的归并排序一起看。自顶向下的归并空间复杂度为O(n),而自底向上的归并可以做到O(n)。不过可以看出链表的归并只分治了一次。

class Solution {//自顶向上
    public ListNode sortList(ListNode head) {
        return sortList(head, null);
    }
    public ListNode sortList(ListNode head, ListNode tail) {
        if (head == null) {//判断链表为空
            return head;
        }
        if (head.next == tail) {//只有一个节点
            head.next = null;
            return head;
        }
        ListNode slow = head, fast = head;//快慢指针寻找中间节点
        while (fast != tail) {
            slow = slow.next;
            fast = fast.next;
            if (fast != tail) {
                fast = fast.next;
            }
        }
        ListNode mid = slow;
        ListNode list1 = sortList(head, mid);
        ListNode list2 = sortList(mid, tail);
        ListNode sorted = merge(list1, list2);
        return sorted;
    }
    //线性合并
    public ListNode merge(ListNode head1, ListNode head2) {
        ListNode dummyHead = new ListNode(0);
        ListNode temp = dummyHead, temp1 = head1, temp2 = head2;
        while (temp1 != null && temp2 != null) {
            if (temp1.val <= temp2.val) {
                temp.next = temp1;
                temp1 = temp1.next;
            } else {
                temp.next = temp2;
                temp2 = temp2.next;
            }
            temp = temp.next;
        }
        if (temp1 != null) {
            temp.next = temp1;
        } else if (temp2 != null) {
            temp.next = temp2;
        }
        return dummyHead.next;
    }
}

自底向上的递归方法就是从左往右,每次对 len 个节点进行线性合并,下一轮再对 len*2 个节点线性合并。力扣讲解的很到位:
在这里插入图片描述

class Solution {//自底向上
    public ListNode sortList(ListNode head) {
        if (head == null) {
            return head;
        }
        int length = 0;
        ListNode node = head;
        while (node != null) {
            length++;
            node = node.next;
        }
        ListNode dummyHead = new ListNode(0, head);
        for (int subLength = 1; subLength < length; subLength <<= 1) {
            ListNode prev = dummyHead, curr = dummyHead.next;
            while (curr != null) {
                ListNode head1 = curr;
                for (int i = 1; i < subLength && curr.next != null; i++) {
                    curr = curr.next;
                }
                ListNode head2 = curr.next;
                curr.next = null;
                curr = head2;
                for (int i = 1; i < subLength && curr != null && curr.next != null; i++) {
                    curr = curr.next;
                }
                ListNode next = null;
                if (curr != null) {
                    next = curr.next;
                    curr.next = null;
                }
                ListNode merged = merge(head1, head2);
                prev.next = merged;
                while (prev.next != null) {
                    prev = prev.next;
                }
                curr = next;
            }
        }
        return dummyHead.next;
    }

    public ListNode merge(ListNode head1, ListNode head2) {
        ListNode dummyHead = new ListNode(0);
        ListNode temp = dummyHead, temp1 = head1, temp2 = head2;
        while (temp1 != null && temp2 != null) {
            if (temp1.val <= temp2.val) {
                temp.next = temp1;
                temp1 = temp1.next;
            } else {
                temp.next = temp2;
                temp2 = temp2.next;
            }
            temp = temp.next;
        }
        if (temp1 != null) {
            temp.next = temp1;
        } else if (temp2 != null) {
            temp.next = temp2;
        }
        return dummyHead.next;
    }
}
  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
链表是一种常见的线性数据结构,它由一系列节点组成,每个节点都包含一个数据元素和一个指向下一个节点的指针。链表的基本操作包括插入、删除、查找等。 1. 插入操作:插入节点时,只需要将新节点插入到链表的适当位置即可。具体实现方式如下: ``` Node* Insert(Node* head, int val) { Node* newNode = new Node(val); if (head == nullptr) return newNode; Node* cur = head; while (cur->next != nullptr) { cur = cur->next; } cur->next = newNode; return head; } ``` 2. 删除操作:删除节点时,需要先找到待删除节点的前一个节点,然后将其指针指向待删除节点的下一个节点。具体实现方式如下: ``` Node* Delete(Node* head, int val) { if (head == nullptr) return nullptr; if (head->val == val) return head->next; Node* cur = head; while (cur->next != nullptr && cur->next->val != val) { cur = cur->next; } if (cur->next != nullptr) cur->next = cur->next->next; return head; } ``` 3. 查找操作:查找节点时,只需要遍历链表,直到找到目标节点即可。具体实现方式如下: ``` Node* Search(Node* head, int val) { Node* cur = head; while (cur != nullptr && cur->val != val) { cur = cur->next; } return cur; } ``` 除了基本操作外,还有其他一些常见操作,例如翻转链表、合并两个有序链表等。需要注意的是,在进行链表操作时,一定要注意指针的变化,以避免出现空指针异常等问题。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值