• LeetCode分类专题(一)——链表1


    iwehdio的博客园:https://www.cnblogs.com/iwehdio/

    学习自:labuladong的算法教程

    本文包括链表leetcode相关题目:

    1. 92、25、234.
    2. 2、19、21、23、24、25、61、82、83、86.

    1、数据结构的基本知识

    数据结构的存储方式

    • 数据结构的存储方式只有两种:数组(顺序存储)和链表(链式存储)。
      • 数组由于是紧凑连续存储,可以随机访问,通过索引快速找到对应元素,而且相对节约存储空间。但正因为连续存储,内存空间必须一次性分配够,所以说数组如果要扩容,需要重新分配一块更大的空间,再把数据全部复制过去,时间复杂度 O(N);而且你如果想在数组中间进行插入和删除,每次必须搬移后面的所有数据以保持连续,时间复杂度 O(N)。
      • 链表因为元素不连续,而是靠指针指向下一个元素的位置,所以不存在数组的扩容问题;如果知道某一元素的前驱和后驱,操作指针即可删除该元素或者插入新元素,时间复杂度 O(1)。但是正因为存储空间不连续,你无法根据一个索引算出对应元素的地址,所以不能随机访问;而且由于每个元素必须存储指向前后元素位置的指针,会消耗相对更多的储存空间。
    • 「队列」、「栈」这两种数据结构既可以使用链表也可以使用数组实现。用数组实现,就要处理扩容缩容的问题;用链表实现,没有这个问题,但需要更多的内存空间存储节点指针。
    • 「图」的两种表示方法,邻接表就是链表,邻接矩阵就是二维数组。邻接矩阵判断连通性迅速,并可以进行矩阵运算解决一些问题,但是如果图比较稀疏的话很耗费空间。邻接表比较节省空间,但是很多操作的效率上肯定比不过邻接矩阵。
    • 「散列表」就是通过散列函数把键映射到一个大数组里。而且对于解决散列冲突的方法,拉链法需要链表特性,操作简单,但需要额外的空间存储指针;线性探查法就需要数组特性,以便连续寻址,不需要指针的存储空间,但操作稍微复杂些。
    • 「树」,用数组实现就是「堆」,因为「堆」是一个完全二叉树,用数组存储不需要节点指针,操作也比较简单;用链表实现就是很常见的那种「树」,因为不一定是完全二叉树,所以不适合用数组存储。为此,在这种链表「树」结构之上,又衍生出各种巧妙的设计,比如二叉搜索树、AVL 树、红黑树、区间树、B 树等等,以应对不同的问题。

    数据结构的基本操作

    • 对于任何数据结构,其基本操作无非遍历 + 访问,再具体一点就是:增删查改。

    • 数据结构种类很多,但它们存在的目的都是在不同的应用场景,尽可能高效地增删查改

    • 如何遍历 + 访问?从最高层来看,各种数据结构的遍历 + 访问无非两种形式:线性的和非线性的。

      • 线性就是 for/while 迭代为代表,非线性就是递归为代表。

      • 数组遍历框架,典型的线性迭代结构:

        void traverse(int[] arr) {
            for (int i = 0; i < arr.length; i++) {
                // 迭代访问 arr[i]
            }
        }
        
      • 链表遍历框架,兼具迭代和递归结构:

        /* 基本的单链表节点 */
        class ListNode {
            int val;
            ListNode next;
        }
        
        void traverse(ListNode head) {
            for (ListNode p = head; p != null; p = p.next) {
                // 迭代访问 p.val
            }
        }
        
        void traverse(ListNode head) {
            // 递归访问 head.val
            traverse(head.next);
        }
        
      • 二叉树遍历框架,典型的非线性递归遍历结构:

        /* 基本的二叉树节点 */
        class TreeNode {
            int val;
            TreeNode left, right;
        }
        
        void traverse(TreeNode root) {
            // 前序遍历
            traverse(root.left)
            // 中序遍历
            traverse(root.right)
            // 后序遍历
        }
        
      • 二叉树框架可以扩展为 N 叉树的遍历框架:

        /* 基本的 N 叉树节点 */
        class TreeNode {
            int val;
            TreeNode[] children;
        }
        
        void traverse(TreeNode root) {
            // 递归访问 root.val
            for (TreeNode child : root.children)
                traverse(child);
        }
        
      • N 叉树的遍历又可以扩展为图的遍历,因为图就是好几 N 叉棵树的结合体。图是可能出现环的,用个布尔数组 visited 做标记就行了。

    2、链表

    • 链表题中的常用命名:
      • pred:前驱;
      • succ:后继;
      • cur:当前;
      • nxt:下一个;
      • head:头;
      • tail:尾。

    递归反转链表的一部分

    • 单链表节点结构:

      // 单链表节点的结构
      public class ListNode {
          int val;
          ListNode next;
          ListNode(int x) { val = x; }
      }
      
    • 什么叫反转单链表的一部分呢,就是给你一个索引区间,让你把单链表中这部分元素反转,其他部分不变:

      image-20210110102040650

      • 注意这里的索引是从 1 开始的。
    • 迭代的思路大概是:

      • 先用一个 for 循环找到第 m 个位置,然后再用一个 for 循环将 m 和 n 之间的元素反转。

      • 具体的,用两个指针指向要反转的两个节点,用第三个指针指向下一个要反转的节点。prev指向反转完成的链表的头,third指向还未反转的链表的头,cur指向反转的节点。

        third = cur.next
        cur.next = prev
        prev = cur
        cur = third
        
      • 迭代实现思路看起来虽然简单,但是细节问题很多的,反而不容易写对。相反,递归实现就很简洁优美。

    • 递归思路:

      • 实现代码:

        ListNode reverse(ListNode head) {
            if (head.next == null) return head;
            ListNode last = reverse(head.next);
            head.next.next = head;
            head.next = null;
            return last;
        }
        
      • 对于递归算法,最重要的就是明确递归函数的定义。具体来说,我们的 reverse 函数定义是这样的:

        • **输入一个节点 head,将「以 **head 为起点」的链表反转,并返回反转之后的头结点
      • 明白了函数的定义,在来看这个问题。比如说我们想反转这个链表:

        image-20210110104302714

      • 那么输入 reverse(head) 后,会在这里进行递归:

        ListNode last = reverse(head.next);
        
      • 不要跳进递归,而是要根据刚才的函数定义,来弄清楚这段代码会产生什么结果

        image-20210110104422798

      • 这个 reverse(head.next) 执行完成后,整个链表就成了这样:

        image-20210110104521962

      • 并且根据函数定义,reverse 函数会返回反转之后的头结点,我们用变量 last 接收了。

      • 现在再来看下面的代码:

        head.next.next = head;
        

        image-20210110173619558

      • 接下来:

        head.next = null;
        return last;
        

        image-20210110104718902

      • 有两个地方需要注意:

        1. 递归函数要有 base case,也就是这句:

          if (head.next == null) return head;
          

          意思是如果链表只有一个节点的时候反转也是它自己,直接返回即可。

        2. 当链表递归反转之后,新的头结点是 last,而之前的 head 变成了最后一个节点,别忘了链表的末尾要指向 null:

          head.next = null;
          
    • 反转链表前n个节点:

      • 实现一个这样的函数:

        // 将链表的前 n 个节点反转(n <= 链表长度)
        ListNode reverseN(ListNode head, int n)
        
      • 比如说对于下图链表,执行 reverseN(head, 3)

        image-20210110105015552

      • 解决思路和反转整个链表差不多,只要稍加修改即可:

        ListNode successor = null; // 后驱节点
        
        // 反转以 head 为起点的 n 个节点,返回新的头结点
        ListNode reverseN(ListNode head, int n) {
            if (n == 1) { 
                // 记录第 n + 1 个节点
                successor = head.next;
                return head;
            }
            // 以 head.next 为起点,需要反转前 n - 1 个节点
            ListNode last = reverseN(head.next, n - 1);
        
            head.next.next = head;
            // 让反转之后的 head 节点和后面的节点连起来
            head.next = successor;
            return last;
        }
        
      • 具体的区别:

        1. base case 变为 n == 1,反转一个元素,就是它本身,同时要记录后驱节点
        2. 刚才我们直接把 head.next 设置为 null,因为整个链表反转后原来的 head 变成了整个链表的最后一个节点。但现在 head 节点在递归反转之后不一定是最后一个节点了,所以要记录后驱 successor(第 n + 1 个节点),反转之后将 head 连接上。
    • 反转列表的一部分:

      • 现在解决我们最开始提出的问题,给一个索引区间 [m,n](索引从 1 开始),仅仅反转区间中的链表元素。

        ListNode reverseBetween(ListNode head, int m, int n)
        
      • 首先,如果 m == 1,就相当于反转链表开头的 n 个元素嘛,也就是我们刚才实现的功能:

        ListNode reverseBetween(ListNode head, int m, int n) {
            // base case
            if (m == 1) {
                // 相当于反转前 n 个元素
                return reverseN(head, n);
            }
            // ...
        }
        
      • 如果 m != 1 怎么办?如果我们把 head 的索引视为 1,那么我们是想从第 m 个元素开始反转对吧;如果把 head.next 的索引视为 1 呢?那么相对于 head.next,反转的区间应该是从第 m - 1 个元素开始的;那么对于 head.next.next 呢……

      • 区别于迭代思想,这就是递归思想,所以我们可以完成代码:

        ListNode reverseBetween(ListNode head, int m, int n) {
            // base case
            if (m == 1) {
                return reverseN(head, n);
            }
            // 前进到反转的起点触发 base case
            head.next = reverseBetween(head.next, m - 1, n - 1);
            return head;
        }
        
    • 总结:

      • 递归的思想处理的技巧是:不要跳进递归,而是利用明确的定义来实现算法逻辑。这里的定义指的是,递归函数的返回值和递归函数中return的值。
      • 处理看起来比较困难的问题,可以尝试化整为零,把一些简单的解法进行修改,解决困难的问题。
      • 值得一提的是,递归操作链表并不高效。和迭代解法相比,虽然时间复杂度都是 O(N),但是迭代解法的空间复杂度是 O(1),而递归解法需要堆栈,空间复杂度是 O(N)。所以考虑效率的话还是使用迭代算法更好。
    • 92题 反转列表 Ⅱ:

      • 递归思路:

        /**
         * Definition for singly-linked list.
         * public class ListNode {
         *     int val;
         *     ListNode next;
         *     ListNode(int x) { val = x; }
         * }
         */
        class Solution {
            public ListNode reverseBetween(ListNode head, int m, int n) {
                if (m==1) return reverseStart(head, n);
                head.next=reverseBetween(head.next, m-1,n-1);
                return head;
            }
        
            ListNode succ = null;
            ListNode reverseStart(ListNode head, int n) {
                if (n==1) {
                    succ=head.next;
                    return head;
                }
                ListNode last = reverseStart(head.next, n-1);
                head.next.next = head;
                head.next=succ;
                return last;
            }
        }
        
      • 迭代思路:

        class Solution {
            public ListNode reverseBetween(ListNode head, int m, int n) {
                if(head==null) return null;
                ListNode pred  = null;
                ListNode cur = head;
                //移动到反转位置
                while(m-1>0){
                    pred = cur;
                    cur = cur.next;
                    m--;
                    n--;
                }
                //con是前一段链表尾,tail是反转部分的链表尾
                ListNode con = pred, tail = cur, temp = null;
                while(n>0){
                    temp = cur.next;
                    cur.next = pred;
                    pred = cur;
                    cur = temp;
                    n--;
                }
                //结束时,pred是反转部分的链表头,cur是下一段链表头
                if(con!=null){
                    con.next=pred;
                } else{
                    head=pred;
                }
                tail.next = cur;
                return head;
        }
        }
        

    K个一组反转链表

    image-20210110144840890

    • 子问题:

      • 这个问题的子问题其实就是将k个长度的链表反转。
      • 还需要考虑的是,反转后,与原链表的连接问题,所以需要维护指向前一段链表的尾和后一段链表的头的指针。
      • 最后,递归或迭代的终止条件,就是当不足k个,则不反转。
      • 例:k=2,前两个反转后,指向后续链表调用反转函数。

      image-20210110150207725

    • 递归向后,迭代反转:

      class Solution {
          public ListNode reverseKGroup(ListNode head, int k) {
              //保存下一段链表的头
              ListNode tail = head;
              for(int i=0; i<k; i++){
                  if(tail==null) return head;
                  tail = tail.next;
              }
              //返回的是反转部分的头
              ListNode newHead = reverse(head, k);
              //head是反转部分的尾
              head.next = reverseKGroup(tail,k);
              return newHead;
          }
      
          ListNode reverse(ListNode head, int num){
              //因为在上一层判断过了,所以head必然不为null
              ListNode cur = head, pred = null, temp = null;
              while(num-->0){
                  temp = cur.next;
                  cur.next = pred;
                  pred = cur;
                  cur = temp;
              }
              return pred;
          }
      }
      

    回文链表

    image-20210110153325442

    • 这道题的关键在于,单链表无法倒着遍历,无法使用双指针技巧。那么最简单的办法就是,把原始链表反转存入一条新的链表,然后比较这两条链表是否相同。

    • 其实,借助二叉树后序遍历的思路,不需要显式反转原始链表也可以倒序遍历链表

    • 树结构不过是链表的衍生。那么,链表其实也可以有前序遍历和后序遍历

      void traverse(ListNode head) {
          // 前序遍历代码
          traverse(head.next);
          // 后序遍历代码
      }
      
    • 如果想倒序遍历链表,就可以在后序遍历位置操作:

      /* 倒序打印单链表中的元素值 */
      void traverse(ListNode head) {
          if (head == null) return;
          traverse(head.next);
          // 后序遍历代码
          print(head.val);
      }
      
    • 模仿双指针实现回文判断:

      class Solution {
          ListNode left = null;
          public boolean isPalindrome(ListNode head) {
              left = head;
              return traverse(head);
          }
          boolean traverse(ListNode right){
              if(right==null) return true;
              boolean res = traverse(right.next);
              res = res &&(left.val==right.val);
              left = left.next;
              return res;
          }
      }
      
    • 这么做的核心逻辑是什么呢?实际上就是把链表节点放入一个栈,然后再拿出来,这时候元素顺序就是反的,只不过我们利用的是递归函数的堆栈而已。

    • 当然,无论造一条反转链表还是利用后序遍历,算法的时间和空间复杂度都是 O(N)。下面我们想想,能不能不用额外的空间,解决这个问题。

    • 优化思路:

      • 通过「双指针技巧」中的快慢指针来找到链表的中点。
      • 如果fast指针没有指向null,说明链表长度为奇数,slow还要再前进一步。
      • 总之,就是让slow指向后半个需要反转的链表头节点。

      image-20210110170209881

      • 最后反转链表后,比较值。
      class Solution {
          public boolean isPalindrome(ListNode head) {
              //设置快慢指针
              ListNode fast = head, slow = head;
              while(fast!=null && fast.next!=null){
                  fast = fast.next.next;
                  slow = slow.next;
              }
              if(fast!=null){
                  slow = slow.next;
              }
              ListNode left = head;
              ListNode right = reverse(slow);
              while(right!=null){
                  if(left.val!=right.val) return false;
                  left = left.next;
                  right = right.next;
              }
              return true;
          }
          //反转从head开始的节点,返回反转后的头结点
          ListNode reverse(ListNode head){
              ListNode cur = head, pred = null, temp = null;
              while(cur!=null){
                  temp = cur.next;
                  cur.next = pred;
                  pred = cur;
                  cur = temp;
              }
              return pred;
          }
      }
      
    • 但是,反转链表会破坏原来链表的结构。

      • 可以通过记录反转部分的头结点,和前半部分的尾节点,通过再进行一次反转恢复结构。

      image-20210110174111814

      p.next = reverse(q);
      

    3、其他链表题目()

    image-20210110174405438

    两数相加

    image-20210110174734087

    image-20210110174740808

    • 直观解法非常简单,就是从后往前对链表中的值相加。

      • 需要考虑的一个是只要链表向后遍历的过程中有一个不是null,或者进位不是0,就说明需要新建一个链表节点。
      • 优化的话,如果其中一个链表为空并且进位为0了,就可以直接把另一个链表接到新链表后面。
      class Solution {
          public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
              ListNode head = new ListNode(0);
              int i = 0, add = 0, sum = 0;
              int v1 = 0, v2 = 0;
              ListNode curr = head;
              while(l1!=null || l2!=null || add>0){
                  if(l1==null){
                      v1 = 0;
                  } else {
                      v1 = l1.val;
                      l1 = l1.next;
                  }
                  if(l2==null){
                      v2 = 0;
                  } else {
                      v2 = l2.val;
                      l2 = l2.next;
                  }
                  sum = v1 + v2 + add;
                  add = sum / 10;
                  curr.next = new ListNode(sum % 10);
                  curr = curr.next;
              }
              return head.next;
          }
      }
      

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

    image-20210110181118980

    • 首先n是有效的,也就是说对n的合法性不需要做判断。

    • 一趟扫描就要完成,需要用到多个指针,慢指针del与快指针last相距n-1。

      • 因为当n=1时,所要删除的就是最后一个节点。而要删除一个节点,最好使能获取其前一个节点的位置,直接把前一个节点的next指向next.next。
      • 然后快慢指针向后遍历,直到快指针的下一个为null,则删除此时慢指针指向的下一个元素。
      • 为了好处理快慢指针的初始化和删除链表第一个节点的问题,引入头哨兵header。
      class Solution {
          public ListNode removeNthFromEnd(ListNode head, int n) {
              ListNode header = new ListNode(0, head);
              ListNode del = header, last = header;
              while(n-->0) {
                  last = last.next;
              }
              while(last.next!=null){
                  last = last.next;
                  del = del.next;
              }
              del.next = del.next.next;
              return header.next;
          }
      }
      

    合并两个有序链表

    image-20210111193058594

    • 迭代思路:

      • 首先同样构造一个头哨兵,从头哨兵开始,最后返回头哨兵的next。
      • 当l1和l2都不为空的时候向后遍历,比较值的大小。cur指向新链表的尾,l1和l2指向旧链表的头。
      • 当有一个为null时,直接将另一个接上就行了。
      class Solution {
          public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
              ListNode header = new ListNode(0);
              ListNode cur = header;
              while(l1!=null && l2!=null){
                  if(l1.val <= l2.val) {
                      cur.next = l1;
                      l1 = l1.next;
                  } else {
                      cur.next = l2;
                      l2 = l2.next;
                  }
                  cur = cur.next;
              }
              if(l1!=null) {
                  cur.next = l1;
              } else {
                  cur.next = l2;
              }
              return header.next;
          }
      }
      
    • 递归思路:

      • 递归基是为null时直接返回。但是因为用到了栈,空间复杂度不如迭代。
      class Solution {
          public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
              if (l1 == null) {
                  return l2;
              } else if (l2 == null) {
                  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;
              }
      
          }
      }
      

    合并K个升序链表

    image-20210111194350452

    • 常规想法,每次比较k个链表头的最小值,复杂度等于顺序合并:

      class Solution {
          public ListNode mergeKLists(ListNode[] lists) {
              ListNode header = new ListNode(0);
              ListNode cur = header;
              while(true) {
                  ListNode min = null;
                  int temp = -1;
                  for(int i=0; i<lists.length; i++) {
                      if(lists[i]==null) {
                          continue;
                      } else {
                          if(min==null || lists[i].val<min.val) {
                              min = lists[i];
                              temp = i;
                          }
                      }               
                  }
                  if(temp == -1) {
                      break;
                  }
                  cur.next = min;
                  cur = cur.next;
                  lists[temp] = cur.next;
              }
              return header.next;
          }
      }
      
    • 相比于顺序合并两个相邻的链表,分治合并的效率更高。

      • 顺序合并:

        image-20210111202426685

      • 分治合并:

        image-20210111202442513

        image-20210111202454403

      • 分治时要注意,这里的递归基有两个,一个是如果区间lo和hi相等,就返回这个链表的指针。另一个是,如果输入数组为空就返回null。

      class Solution {
          public ListNode mergeKLists(ListNode[] lists) {
              return merge(lists, 0, lists.length-1);
          }
          ListNode merge(ListNode[] lists, int lo, int hi) {
              if(lo==hi) {
                  return lists[lo];
              }
              if(lo>hi) {
                  return null;
              }
              int mid = (lo+hi)/2;
              return mergeTwoLists(merge(lists, lo, mid), merge(lists, mid+1, hi));
          }
      
          ListNode mergeTwoLists(ListNode l1, ListNode l2) {
              ListNode header = new ListNode(0);
              ListNode cur = header;
              while(l1!=null && l2!=null){
                  if(l1.val <= l2.val) {
                      cur.next = l1;
                      l1 = l1.next;
                  } else {
                      cur.next = l2;
                      l2 = l2.next;
                  }
                  cur = cur.next;
              }
              if(l1!=null) {
                  cur.next = l1;
              } else {
                  cur.next = l2;
              }
              return header.next;
          }
      }
      

    两两交换链表中的节点

    image-20210111204106202

    • 递归形式,就是反转链表的变种:

      • 递归基是如果传入的链表只有一个节点或为null,就直接返回。
      • 否则,交换并链接头尾。
      class Solution {
          public ListNode swapPairs(ListNode head) {
              if(head==null || head.next==null){
                  return head;
              } else {
                  ListNode temp = head.next.next, ans = head.next;
                  head.next.next = head;
                  head.next = swapPairs(temp);
                  return ans;
              }  
          }
      }
      
    • 迭代形式:

      • 迭代比较麻烦的就是处理null的问题。
      class Solution {
          public ListNode swapPairs(ListNode head) {
              if(head==null || head.next==null) return head;
              ListNode header = new ListNode(0, head), curr = head, tail = head.next.next, temp = null, ans=header;
              while(true) {
                  temp = curr.next;
                  temp.next = curr;
                  curr.next = tail;
                  header.next = temp;
                  if(tail==null || tail.next==null){
                      return ans.next;
                  } else {
                      header = header.next.next;
                      curr = tail;
                      tail = tail.next.next;
                      
                  }
              }
          }
      }
      

    旋转链表

    image-20210111210100391

    • 最需要注意的一点就是,先首尾相连把链表变成环,再移动ans指针。不然的话需要单独处理shift为0的情况:

      class Solution {
          public ListNode rotateRight(ListNode head, int k) {
              if(head==null || head.next==null) return head;
              ListNode tail=head, ans=head, temp=null;
              int len = 1;
              while(tail.next!=null){
                  tail = tail.next;
                  len++;
              }
              tail.next = head;
              int shift = len - k % len;
              while(--shift>0){
                  ans = ans.next;
              }
              temp = ans;
              ans = ans.next;
              temp.next = null;
              return ans;
          }
      }
      

    删除链表中的重复元素

    image-20210111212920821

    • 用递归的方式,每次传入处理好的链表尾和需要删除的链表头。

      • 检查链表头是否有重复的,有就删除。如果有的话需要特别注意null值的判定和删除后的指向。
      class Solution {
          public ListNode deleteDuplicates(ListNode head) {
              ListNode header = new ListNode(0, head);
              deleteD(header, head);
              return header.next;
          }
          void deleteD(ListNode header, ListNode head) {
              if(head==null || head.next==null) return;
              if(head.next!=null && head.val == head.next.val) {
                  while(head.next!=null && head.val == head.next.val) {
                      head = head.next;
                  }
                  header.next = head.next;
                  head = header;
              } 
              deleteD(head, head.next);
          }
      }
      

    删除排序链表中的重复元素

    image-20210111220639548

    • 与前一道题不同的是,重复的需要留下一个,其实就是把上一道题的header.next = head.next改为header.next = head。

      class Solution {
          public ListNode deleteDuplicates(ListNode head) {
              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;
          }
      }
      

    分隔链表

    image-20210111221254957

    • 分别用哨兵引出小于x和大于x的链:

      class Solution {
          public ListNode partition(ListNode head, int x) {
              ListNode header1 = new ListNode(0), header2 = new ListNode(0), cur1 = header1, cur2 = header2;
              while(head!=null) {
                  if(head.val<x){
                      cur1.next = head;
                      cur1 = cur1.next;
                  } else {
                      cur2.next = head;
                      cur2 = cur2.next;
                  }
                  head = head.next;
              }
              cur1.next = header2.next;
              cur2.next = null;
              return header1.next;
          }
      }
      

    iwehdio的博客园:https://www.cnblogs.com/iwehdio/
  • 相关阅读:
    C#中异步和多线程的区别
    猫 老鼠 人的编程题
    C#中数组、ArrayList和List三者的区别
    经典.net面试题目
    sql有几种删除表数据的方式
    内存池的实现
    A*算法为什么是最优的
    传教士与野人问题
    d3d导致cairo不正常
    c++中的signal机制
  • 原文地址:https://www.cnblogs.com/iwehdio/p/14264733.html
Copyright © 2020-2023  润新知