Java数据结构第二讲-数组/链表

简介: Java数据结构第二讲-数组/链表

5、数组部分面试题

定义是多个相同类型数据按一定顺序排列的集合,并使用一个名字命名,并通过编号的方式对这些数据进行统一管理。

  • 1、实现一个支持动态扩容的数组
  • 2、实现一个大小固定的有序数组,支持动态增删改操作 实际开发中我们使用ArrayList,更高效
  • 3、实现两个有序数组合并为一个有序数组
  • 4、数组操作常见问题(数组脚标越界异常(ArrayIndexOutOfBoundsException)/空指针异常(NullPointerException))
  • leetcode15:三数求和
    给定一个包含 n 个整数的数组 nums,判断 nums 中是否存在三个元素 a,b,c ,使得 a + b + c = 0 ?找出所有满足条件且不重复的三元组
    思路:首先对数据进行排序,然后确定第一个数,使用for循环,后两个数使用两指针,依次尝试,如果值大于0-num[i],右指针左移;如果值小于0-num[i],左指针右移。
class Solution {
  public List<List<Integer>> threeSum(int[] nums) {
    Arrays.sort(nums);//由小到大
    List<List<Integer>> ls = new ArrayList<>();
    for (int i = 0; i < nums.length - 2; i++) {
      if (i == 0 || (i > 0 && nums[i] != nums[i - 1])) {  // 跳过可能重复的答案
        int l = i + 1, r = nums.length - 1, sum = 0 - nums[i];
        while (l < r) {
          if (nums[l] + nums[r] == sum) {
            ls.add(Arrays.asList(nums[i], nums[l], nums[r]));
            while (l < r && nums[l] == nums[l + 1]) l++;
            while (l < r && nums[r] == nums[r - 1]) r--;
            l++;
            r--;
          } else if (nums[l] + nums[r] < sum) {
            while (l < r && nums[l] == nums[l + 1]) l++;   // 跳过重复值
            l++;
          } else {
            while (l < r && nums[r] == nums[r - 1]) r--;
            r--;
          }
        }
      }
    }
    return ls;
  }
}//时间复杂度是O(n^2)
  • leetcode169:求众数 给定一个大小为n的数组,找到其中的众数。众数是指在数组中出现次数大于?n/2?的元素
    先决条件:给定的数组总是存在众数
    思路:1、利用摩尔投票法 2、利用java的api
public int majorityElement(int[] nums){
  int count = 1;
  int maj = nums[0];
  for (int i = 1; i < nums.length; i++){
    if (maj == nums[i])
      count++;
    else {
      count--;
      if (count == 0) {//说明maj所代表的数不能超过一半
        maj = nums[i + 1];
      }
    }
  }//时间复杂度O(n)
  return maj;
}
  • 第二种解法:使用java的api,排序
public int majorityElement(int[] nums){
  Arrays.sort(nums);//时间复杂度O(nlgn)
  return nums[nums.length / 2];
}
  • LeetCode41:求缺失的第一个正数
    给定一个未排序的整数数组,找出其中没有出现的最小的正整数。
class Solution {
  public int firstMissingPositive(int[] nums) {
    //先排序,然后分两种情况 :有1  和  没有1 (负数略过)
    //1.没有1,则输出1
    //2.有1 则判断下一个数和前一个数是否相等、差1或者差好几个数,相等继续,差1继续,否则退出
    boolean flag = false;
    int i;
    Arrays.sort(nums);
    for(i=0;i<nums.length;i++)
    {
      if(nums[i]<0)
        continue;//负数略过
      if(nums[i]==1)
        flag=true;
      if(i+1<nums.length && nums[i]==nums[i+1])
        continue;
      if(i+1==nums.length || nums[i]+1!=nums[i+1])
          break;
    }
    if(flag==true)
      return nums[i]+1;
    if(flag==false)
      return 1;
    return 0;
  }
}//时间复杂度O(n)

Demo 五子棋程序中,有存盘退出和续上盘的功能,因为该二维数组中的很多值默认为0,因此记录了很多没有意义的数据 --》稀疏


6、链表部分面试题

链表使用场景

git中的分支管理

  • 当使用 git commit 进行提交操作时,Git 会先计算每一个子目录(本例中只有项目根目录)的校验和, 然后在 Git 仓库中这些校验和保存为树对象。随后,Git 便会创建一个提交对象, 它除了包含上面提到的那些信息外,还包含指向这个树对象(项目根目录)的指针。 如此一来,Git 就可以在需要的时候重现此次保存的快照。
  • 现在,Git 仓库中有五个对象:三个 blob 对象(保存着文件快照)、一个 树 对象 (记录着目录结构和 blob 对象索引)以及一个 提交 对象(包含着指向前述树对象的指针和所有提交信息)
  • 做些修改后再次提交,那么这次产生的提交对象会包含一个指向上次提交对象(父对象)的指针
  • Git 的分支,其实本质上仅仅是指向提交对象的可变指针。 Git 的默认分支名字是 master。 在多次提交操作之后,你其实已经有一个指向最后那个提交对象的 master 分支。 master 分支会在每次提交时自动向前移动

6.1、单链表:next指针 (尾结点特殊的地方是:指针不是指向下一个结点,而是指向一个空地址NULL,表示这是链表上最后一个结点)
public class ListNode {
  int val;
  ListNode next;
  ListNode(int x) {
    val = x;
  }
}

循环链表:循环链表的优点是从链尾到链头比较方便。当要处理的数据具有环型结构特点时,就特别适合采用循环链表(比如著名的约瑟夫问题)

ListNode p = null;//在单链表的基础之上,链尾指向链头
q =p;
for (int i = 2; i <= N; i++) {
  p = p.getNext();
  p.setVal(i);
}
p.setNext(q);//构建循环链表

在遍历循环链表时得特别小心,否则将会无限地遍历链表,因为循环链表每一个结点都有一个后继结点

双向链表:(需要额外的两个空间来存储后继结点next和前驱结点的地址prev)

public class ListNode {
  int value;
  ListNode prev;
  ListNode next;
  ListNode(int key, int val) {
    this.key = key;
    this.value = val;
  }
}

使用技巧:

1、理解指针或引用的含义:是存储所指对象的内存地址(将某个变量赋值给指针,实际上就是将这个变量的地址赋值给指针)

2、警惕指针丢失和内存泄漏 java不需考虑(使用jvm自动管理内存)

3、利用哨兵简化实现难度:如果我们引入哨兵结点,在任何时候,不管链表是不是空,head指针都会一直指向这个哨兵结点(插入排序、归并排序、动态规划)

删除最后一个结点和删除其他节点,插入第一个结点和插入其他节点可以统一为相同的代码逻辑。

哨兵的好处:它可以减少特殊情况的判断,比如判空,判越界,因为空可越界可认为是小概率情况,如实每次执行代码都走一遍,大多数情况下是多于的。

比如给一个哨兵节点,以及将key赋值给末尾元素,让数组遍历不用判断越界也可以因为相等停下来。

4、重点留意便捷条件处理:(如果链表为空时,代码是否能正常工作?如果链表只包含一个结点时,代码是否能正常工作?代码逻辑在处理头结点和尾结点的时候,是否能正常工作?)

5、举例画图,辅助思考:(举例法和画图法)


6.2、描述一下链式存储结构
  • 可以用任意一组存储单元来存储单链表中的数据结构(可以不连续),存储每个元素的值a,还必须存储后集结点的信息,这两个信息组成结点。

6.3、倒排一个LinkedList(即链表的反转) LeetCode 21

开发中使用集合工具包:Collections.reverse(List<?> list)

原理:i m n相邻,调整指针的指向,调整m的指向,指向结点i,链表会断开,需要在调整之前把n保存起来 代码P236

public class 链表反转 {
//单链表的反转 调整指针的指向,在调整next指针之前,需要保存前一个值 反转后链表的头结点为原始链表的尾节点,即next为空指针的节点
  public void reverseIteratively(Node head) {
    Node pReversedHead = head;
    Node pNode = head;
    Node pPrev = null;
    while (pNode != null) {
      Node pNext = pNode.next;
      if (pNext == null) {
        pReversedHead = pNode;//pNode此时为最后一个结点 反转后链表的头结点为原始链表的尾节点
      }
      pNode.next = pPrev;
      pPrev = pNode;
      pNode = pNext;
    }
    head = pReversedHead;
}

6.4、判断一个单链表中是否有环? 阿里 LeetCode141
  • 思路1:蛮力法
    若链表中出现多个结点的后继指针重复,就表明存在环。从第一个结点开始,令其为当前节点,然后看看链表中其他节点的后继指针是否指向当前结点,如果存在,说明链表中存在环。
    缺点:如果不能确定链表的表尾,算法将会出现死循环。

*思路2:使用散列表(时间复杂度O(n),空间复杂度O(n))

从表头节点开始,逐一遍历链表中的每个结点;

对于每个结点,检查该结点的地址是否存在于散列表中;

如果存在,则表明当前访问的结点已经被访问过,出现此情况的原因是给定的链表中存在环;

如果散列表中没有当前节点的地址,那么把该地址插入散列表中;

重复上述过程,直至到达表尾或找到环。

  • 思路3:如果一个单链表中有环,用一个指针去遍历,永远不会结束,所以可以用两个指针,一个指针一次走一步,另一个指针一次走两步,如果存在环,则这两个指针会在环内相遇,时间复杂度为O(n) indeed(无论环的个数是奇数还是偶)被称为Floyd算法
public static  boolean checkCircle(Node list){
  if (list == null) {
    return false;
  }
  Node fast = list.next;
  Node slow = list;
  while (fast != null && fast.next !=null) {
    fast = fast.next.next;
    slow = slow.next;
    if (slow ==fast) {
      return true;
    }
  }        
  return false;
}//时间复杂度O(n) 空间复杂度O(1)

对floyd算法的补充:如果两个指针每次分别移动2个结点和3个结点,而不是移动一个和2个结点,算法仍然有效吗?

可以,算法的复杂度可能增加


6.5、判定给定的链表是否已NULL结束,如果链表中存在环,返回环的长度?

思路:在找到链表中的环后,保持slowPtr指针不变,fastPtr指针则继续移动,每次移动fastPtr指针时,计数器变量加1,直至再一次回到slowPtr指针所在的位置,即为环的长度。

public class 检测环的长度 {
  int FindLoopLength(ListNode head){
    ListNode slowPtr =head,fastPtr =head;
    boolean loopExists = false;
    int counter = 0;
    if (head == null) {
      return 0;
    }
    while (fastPtr.next != null && fastPtr.next.next != null) {
      slowPtr = slowPtr.next;
      fastPtr = fastPtr.next.next;
      if (slowPtr == fastPtr) {
        loopExists =true;
        break;
      }
    }
    if (loopExists) {
      fastPtr =fastPtr.next;
      while (slowPtr != fastPtr) {
        fastPtr =fastPtr.next;
        counter++;
      }
      return counter;
    }
    return 0;  //链表中不存在环
  }
} //时间复杂度O(n)

补充:此思路可以引申为 求循环小数的开始位置(小数点之后的位数)和循环长度


6.6、快慢指针能解决的问题? 阿里
  • 1、已知单链表的头指针,查找到倒数第K个节点,然后删除这个节点
    思路1:快慢指针法:
    我们定义一个快指针P和慢指针Q,先让P指针走到K个节点位置,然后Q指针从头指针开始和P一起移动,当P移动到尾部的时候,那么此时Q节点所在的位置就是倒数第K个节点
public static Node deleteLastKth(Node list,int k){
  Node fast =list;
  int i =1;
  while (fast !=null && i<k) {
    fast =fast.next;
    ++i;//第一个指针先走k步
  }
  if (fast ==null) {
    return list;
  }
  Node slow =list;
  Node prev =null;
  while (fast.next !=null) {
    fast = fast.next;
    prev =slow;  //prev为倒数第k个数
    slow =slow.next;
  }
  if (prev ==null) {
    list = list.next;
  }else {
    prev.next =prev.next.next;
  }
  return list;
}//时间复杂度O(n)

思路2:蛮力法(时间复杂度最高)

从链表的第一个结点开始,统计当前节点后面的结点个数。如果后面的节点个数小于k-1,算法结束;如果大于k-1,则移动到下一个结点,重复该过程

思路3:散列表O(m) 为了减少链表遍历的次数

散列表的条目是<结点的位置,结点地址>,在遍历链表时,可以得到链表的长度,令M表示链表的长度,这样求链表的导师胡第n个结点的问题转变为求链表正数

第M-n+1个结点。返回散列表中主键为M-n+1的值即可。时间复杂度O(m),空间复杂度O(m):创建一个大小为M的散列表。

  • 2、已知单链表的头结点,查找到链表的中间节点(只允许扫描一次)
    一个快指针P和慢指针Q,P和Q同时从头指针出发,快指针P每次移动两步,慢指针每次移动一步,当快指针P到尾部的时候,慢指针Q所在的位置就是中间节点的位置
public class 找到链表的中间节结点 {
  ListNode FindMiddle(ListNode head) {
    ListNode ptr1x, ptr2x;
    ptr1x = ptr2x = head;
    int i = 0;
    //不断循环,直至第一个指针到达表尾
    while (ptr1x.getNext() !=null) {
      if (i == 0) {
        ptr1x =ptr1x.getNext();//只移动第一个指针
        i = 1;
      }
      else if (i== 1) {
        ptr1x = ptr1x.getNext();
        ptr2x = ptr2x.getNext();
        i =0;
      }
    }        
    return ptr2x;//返回ptr2x的值,即为中间结点
  }
}//时间复杂度O(n)  空间复杂度O(1)

6.7、实现两个有序的链表合并为一个有序链表(双重遍历) LeetCode23 合并k个排序链表

思路:使用分治的思想,两两归并

class Solution {
   public ListNode mergeKLists(ListNode[] lists) {
    if (lists.length == 0)
      return null;
    if (lists.length == 1)
      return lists[0];
    if (lists.length == 2) {
      return mergeTwoLists(lists[0], lists[1]);
    }
    int mid = lists.length/2;
    ListNode[] l1 = new ListNode[mid];
    for(int i = 0; i < mid; i++){
      l1[i] = lists[i];
    }
    ListNode[] l2 = new ListNode[lists.length-mid];
    for(int i = mid,j=0; i < lists.length; i++,j++){
      l2[j] = lists[i];
    }
    return mergeTwoLists(mergeKLists(l1),mergeKLists(l2));
  }
  //两个有序链表合并为一个新的有序链表  递归的方法
  public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
    if (l1 == null) return l2;
    if (l2 == null) return l1;
    ListNode head = null;
    if (l1.val <= l2.val){
      head = l1;
      head.next = mergeTwoLists(l1.next, l2);
    } else {
      head = l2;
      head.next = mergeTwoLists(l1, l2.next);
    }
    return head;
  }
}

6.8、在有序链表中插入一个结点
public class 在有序链表中插入一个结点 {
      ListNode InsertSortedList(ListNode head, ListNode newNode){
        ListNode current =head;
        ListNode temp = null;
        if (head ==null) {
          return newNode;
        }
        //遍历链表,直至找到比新节点中数据值更大的节点
        while (current != null && current.val < newNode.val) {
          temp = current;//temp为current的上一个节点
          current = current.next; //current为比newNode值大的数
        }
        //在该结点前插入新节点
        newNode.setNext(current);
        temp.setNext(newNode);
        return null;  
      }   
    }//时间复杂度O(n)

6.9、求两个单向链表的合并点,合并后成为一个单向链表。假设链表list1和链表list2在相交前的节点数量分别为n和m,n/m大小不确定,求两个链表的合并点。

方法1:蛮力法

把第一个链表中的每一个结点指针与第二个链表中的每一个结点指针比较,当结点相等时,即为相交结点。时间复杂度为O(mn)

方法2:散列表

选择结点较少的链表(若链表长度未知,那么随便选择一个链表),将其所有结点的指针值保存在散列表中;遍历另一个链表,对于该链表中的每一个结点,检查散列表

中是否已经保存了其结点指针。如果两个链表存在合并点,那么必定会在散列表中找到记录。时间复杂度O(m)+O(n);空间复杂度O(m)或O(n)

方法3:两个栈

创建两个栈,然后遍历两个链表,分别把所有结点存入第一个和第二个栈,两个栈包含了对应链表的结点地址,比较两个栈的栈顶元素,如果相等,则弹出两个栈

的栈顶元素并保存在临时变量中,继续上述操作,直至两个栈的栈顶元素不相等,此时即找到了两个链表的合并点。时间复杂度O(m+n),空间复杂度O(m+n)

方法4:时间复杂度超低的解法

获取两个链表L1/L2的长度,O(max(m,n));计算两个长度的差d,从较长链表的表头开始,移动d步,然后两个链表同时移动,直至出现两个后继指针相等的情况。

public class 求两个链表的合并点 {
        ListNode FindIntersectingNode(ListNode list1, ListNode list2){
          int L1=0,L2=0,diff=0;//L1为第一个链表的长度,L2为第二个链表的长度,diff为两链表的差值
          ListNode head1=list1,head2=list2;
          while (head1 !=null) {
            L1++;
            head1 = head1.getNext();
          }
          while (head2 !=null) {
            L2++;
            head2 = head2.getNext();
          }
          if (L1<L2) {
            head1 = list2;
            head2 = list1;
            diff = L2-L1;
          }
          else  {
            head1 = list1;
            head2 = list2;
            diff = L1-L2;
          }
          for (int i = 0; i < diff; i++) {
            head1 = head1.getNext();
          }
          while (head1 != null && head2 != null) {
            if (head1 == head2) {
               return head1;
            }
            head1= head1.getNext();
            head2 = head2.getNext();
          }
          return null;
        }
      }//时间复杂度O(max(m,n)) 空间复杂度O(1)

6.10、如何判断一个字符串(链表)是否是回文字符串的问题(字符串是通过单链表来存储)(上海自来水来自海上)

1)前提:字符串以单个字符的形式存储在单链表中。

2)遍历链表,判断字符个数是否为奇数,若为偶数,则不是。

3)将链表中的字符倒序存储一份在另一个链表中。

4)同步遍历2个链表,比较对应的字符是否相等,若相等,则是水仙花字串,否则,不是。

思路2:使用快慢两个指针找到链表中点,慢指针每次前进一步,快指针每次前进两步。在慢指针前进的过程中,同时修改其 next 指针,使得链表前半部分反序。最后比较中点两侧的链表是否相等

时间复杂度O(n) 空间复杂度O(1)


6.11、O(1)时间内删除单链表中某一个节点

把后一个元素赋值给待删除节点,这样也就相当于是删除了当前元素

1. 如果待删除节点不是最后一个节点,就用他的next节点的value覆盖它的value,然后删掉它的next节点

2、如果是最后一个节点,顺序遍历o(n)


6.12、如何逐对逆置链表?初始1->2->3->4->X,逐对转置后,为2->1->4->3->X。
//递归版本
ListNode ReversePairRecursive(ListNode head){
  ListNode temp;
  if (head ==null || head.next == null) {
    return head;  //当前链表为空或只有一个元素
  }else {
    //逆置第一对
    temp = head.next;
    head.next = temp.next;//第一个结点的下一个为第三个结点
    temp.next = head;//第一个结点变为第二个
    head =temp;//第二个结点变第一个
    head.next.next=ReversePairRecursive(head.next.next);
    return head;
  }
}

6.13、约瑟夫环(N个人想选出一个领头人,他们排成一个环,沿着环每数到第M个人就排除该人,并从下一个人开始重新数,求最后留在环中的人)
/**
     * @param N 人数
     * @param M 需要排除的人序号
     * @return 最后留下来的人
     */
    ListNode GetJosephusPosition(int N, int M){
      ListNode p = null,q;
      //建立一个包含所有人的循环链表
      p.setVal(1);
      q =p;
      for (int i = 2; i <= N; i++) {
        p = p.getNext();
        p.setVal(i);
      }
      p.setNext(q);//构建循环链表
      for (int count = N; count >1; --count) {
        for (int i = 0; i < M-1; i++) {
          p = p.getNext();
        }
        p.setNext(p.getNext().getNext());//删除选手
      }
      return p;//最后留下的勇者
    }
相关文章
|
9月前
|
存储 缓存 Java
Java数组全解析:一维、多维与内存模型
本文深入解析Java数组的内存布局与操作技巧,涵盖一维及多维数组的声明、初始化、内存模型,以及数组常见陷阱和性能优化。通过图文结合的方式帮助开发者彻底理解数组本质,并提供Arrays工具类的实用方法与面试高频问题解析,助你掌握数组核心知识,避免常见错误。
|
前端开发 Java
java实现队列数据结构代码详解
本文详细解析了Java中队列数据结构的实现,包括队列的基本概念、应用场景及代码实现。队列是一种遵循“先进先出”原则的线性结构,支持在队尾插入和队头删除操作。文章介绍了顺序队列与链式队列,并重点分析了循环队列的实现方式以解决溢出问题。通过具体代码示例(如`enqueue`入队和`dequeue`出队),展示了队列的操作逻辑,帮助读者深入理解其工作机制。
606 1
|
8月前
|
Java
Java 数组学习笔记
本文整理Java数组常用操作:遍历、求和、查找、最值及二维数组行求和等典型练习,涵盖静态初始化、元素翻倍、去极值求平均等实例,帮助掌握数组基础与应用。
|
10月前
|
存储 Java 索引
java 数组
在 Java 中,数组是一种数据结构,用于存储多个相同类型的数据元素。数组的大小一旦创建后就不能改变,因此它是固定长度的。Java 数组是一种 对象,即使它存储的值是基本类型(如 int、double 等),它也是一个对象引用。
232 0
|
11月前
|
存储 安全 Java
Java 集合面试题从数据结构到 HashMap 源码剖析详解及长尾考点梳理
本文深入解析Java集合框架,涵盖基础概念、常见集合类型及HashMap的底层数据结构与源码实现。从Collection、Map到Iterator接口,逐一剖析其特性与应用场景。重点解读HashMap在JDK1.7与1.8中的数据结构演变,包括数组+链表+红黑树优化,以及put方法和扩容机制的实现细节。结合订单管理与用户权限管理等实际案例,展示集合框架的应用价值,助你全面掌握相关知识,轻松应对面试与开发需求。
504 3
|
存储 Java 索引
Java快速入门之数组、方法
### Java快速入门之数组与方法简介 #### 一、数组 数组是一种容器,用于存储同种数据类型的多个值。定义数组时需指定数据类型,如`int[]`只能存储整数。数组的初始化分为静态和动态两种: - **静态初始化**:直接指定元素,系统自动计算长度,如`int[] arr = {1, 2, 3};` - **动态初始化**:手动指定长度,系统给定默认值,如`int[] arr = new int[3];` 数组访问通过索引完成,索引从0开始,最大索引为`数组.length - 1`。遍历数组常用`for`循环。常见操作包括求和、找最值、统计特定条件元素等。
|
人工智能 Java
Java 中数组Array和列表List的转换
本文介绍了数组与列表之间的相互转换方法,主要包括三部分:1)使用`Collections.addAll()`方法将数组转为列表,适用于引用类型,效率较高;2)通过`new ArrayList&lt;&gt;()`构造器结合`Arrays.asList()`实现类似功能;3)利用JDK8的`Stream`流式计算,支持基本数据类型数组的转换。此外,还详细讲解了列表转数组的方法,如借助`Stream`实现不同类型数组间的转换,并附带代码示例与执行结果,帮助读者深入理解两种数据结构的互转技巧。
982 1
Java 中数组Array和列表List的转换
|
存储 监控 Java
《从头开始学java,一天一个知识点》之:数组入门:一维数组的定义与遍历
**你是否也经历过这些崩溃瞬间?** - 看了三天教程,连`i++`和`++i`的区别都说不清 - 面试时被追问&quot;`a==b`和`equals()`的区别&quot;,大脑突然空白 - 写出的代码总是莫名报NPE,却不知道问题出在哪个运算符 这个系列就是为你打造的Java「速效救心丸」!我们承诺:每天1分钟,地铁通勤、午休间隙即可完成学习;直击痛点,只讲高频考点和实际开发中的「坑位」;拒绝臃肿,没有冗长概念堆砌,每篇都有可运行的代码标本。明日预告:《多维数组与常见操作》。 通过实例讲解数组的核心认知、趣味场景应用、企业级开发规范及优化技巧,帮助你快速掌握Java数组的精髓。
513 23
|
12月前
|
存储 人工智能 Java
打乱数组内容引发的问题( Java)
本文介绍了两种实现数组随机打乱的方法,并深入探讨了Java中原始数据类型与对象类型的差异。方法一通过自定义随机数交换数组元素位置,方法二借助`Collections.shuffle()`函数完成数组打乱。同时,文章详细解析了`int`和`Integer`的区别,包括声明方式、内存占用、初始化以及对象特性等,并讲解了自动装箱与拆箱的功能,帮助读者更好地理解Java的基础知识。
198 0