List的介绍
在集合框架中,List是一个接口,继承自Collection。
站在数据结构的角度来看,List就是一个线性表,即n个具有相同类型元素的有限序列,在该序列上可以执行增删改查以及变量等操作。
线性表在逻辑上是线性结构,也就是连续的一条直线。但是在物理结构上并不一定是连续的,线性表通常以数组和链式结构的形式存储。
List常见方法如下
方法 | 介绍 |
---|---|
boolean add(E e) | 尾插 e |
void add(int index, E element) | 将 e 插入到 index 位置 |
boolean addAll(Collection<? extends E> c) | 尾插 c 中的元素 |
E remove(int index) | 删除 index 位置元素 |
boolean remove(Object o) | 删除遇到的第一个 o |
E get(int index) | 获取下标 index 位置元素 |
E set(int index, E element) | 将下标 index 位置元素设置为 element |
void clear() | 清空 |
boolean contains(Object o) | 判断 o 是否在线性表中 |
int indexOf(Object o) | 返回第一个 o 所在下标 |
int lastIndexOf(Object o) | 返回最后一个 o 的下标 |
List subList(int fromIndex, int toIndex) | 截取部分 list |
注意:List是个接口,并不能直接用来实例化。
如果要使用,必须去实例化List的实现类。在集合框架中,ArrayList和LinkedList都实现了List接口。
subList方法需要单独说说参考此文
1.参数是前闭后开的
2.ArrayList调用subList的返回值是不可强转成ArrayList的,否则会抛出ClassCastException 异常,
说明:subList 返回的是 ArrayList 的内部类 SubList,不是 ArrayList ,而是 ArrayList 的一个视图,对于 SubList 子列表的所有操作最终会反映到原列表上。
3.修改sublist会影响原来的list
4.修改原list,则sublist的所有操作会报错
5.subList返回一个镜像而不是新对象
public class Test {
public static void main(String[] args) {
ArrayList<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
List<Integer> sublist= list.subList(0,2);
System.out.println(list);
System.out.println(sublist);
System.out.println("---------------");
sublist.add(4);
System.out.println(list);
System.out.println(sublist);
System.out.println("---------------");
sublist.set(0, 10);
System.out.println(list);
System.out.println(sublist);
}
}
顺序表
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储, 在数组上完成数据的增删查改。
List<Integer> arrayList1 = new ArrayList<>();
ArrayList<Integer> arrayList2 = new ArrayList<>();
arrayList1只能调用接口中的方法
arrayList2可以调用当前类自己的方法和接口中的方法
ArrayList的构造
1.指定顺序表初始容量
- 如果参数大于0,则以参数为容量构造
- 如果参数等于0,则构造空数组
- 如果参数小于0,则抛出异常
2.无参构造
构造空数组(没有分配大小),但add方法有检查是否扩容
3.利用 Collection 构建 ArrayList
传入的参数是继承Collection接口的,泛型是E类型或者E的子类
可以看看我写的集合框架和泛型的文章
ArrayList的遍历
ArrayList 可以使用三种方式遍历:for循环+下标、foreach、使用迭代器
import java.util.ArrayList;
import java.util.List;
import java.util.ListIterator;
public class Test {
public static void main(String[] args) {
List<Integer> list = new ArrayList<>();
list.add(1);
list.add(2);
list.add(3);
// 使用下标+for遍历
for (int i = 0; i < list.size(); i++) {
System.out.print(list.get(i) + " ");
}
System.out.println();
// 借助foreach遍历
for (Integer integer : list) {
System.out.print(integer + " ");
}
System.out.println();
//使用迭代器
//1.从前往后
ListIterator<Integer> it1 = list.listIterator();
while(it1.hasNext()){
System.out.print(it1.next() + " ");
}
System.out.println();
//2.从后往前
ListIterator<Integer> it2 = list.listIterator(list.size());
while(it2.hasPrevious()){
System.out.print(it2.previous() + " ");
}
}
}
ArrayList的扩容机制
可以看一下add方法中的ensureCapacityInternal
这里就直接给结论了:
- 检测是否真正需要扩容,如果是调用grow方法准备扩容
- 初步预估按照1.5倍大小扩容
如果用户所需大小超过预估1.5倍大小,则按照用户所需大小扩容
真正扩容之前检测是否能扩容成功,防止太大导致扩容失败 - 使用copyOf进行扩容
(1)原地移除数组中所有的元素val,要求时间复杂度为O(N),空间复杂度为O(1)。
oj链接
给你一个数组 nums 和一个值 val,你需要 原地 移除所有数值等于 val 的元素,并返回移除后数组的新长度。
不要使用额外的数组空间,你必须仅使用 O(1) 额外空间并原地修改输入数组。
元素的顺序可以改变。你不需要考虑数组中超出新长度后面的元素。
双指针
class Solution {
public int removeElement(int[] nums, int val) {
int left = 0;
for (int right = 0; right < nums.length; right++) {
if (nums[right] != val) {
nums[left++] = nums[right];
}
}
return left;
}
}
(2)删除有序数组中的重复项
oj链接
给你一个 非严格递增排列 的数组 nums ,请你 原地 删除重复出现的元素,使每个元素 只出现一次 ,返回删除后数组的新长度。元素的 相对顺序 应该保持 一致 。然后返回 nums 中唯一元素的个数。
考虑 nums 的唯一元素的数量为 k ,你需要做以下事情确保你的题解可以被通过:
更改数组 nums ,使 nums 的前 k 个元素包含唯一元素,并按照它们最初在 nums 中出现的顺序排列。nums 的其余元素与 nums 的大小不重要。
返回 k
双指针
class Solution {
public int removeDuplicates(int[] nums) {
if(nums == null || nums.length == 0) return 0;
int front = 0;
int next = 1;
while(next < nums.length){
if(nums[front] != nums[next]){
nums[++front] = nums[next];
}
next++;
}
return front+1;
}
}
优化(相邻的元素不同不进行多余的赋值操作)
像[1,2,3,4,5]就不用进行赋值操作
class Solution {
public int removeDuplicates(int[] nums) {
if(nums == null || nums.length == 0) return 0;
int front = 0;
int next = 1;
while(next < nums.length){
if(nums[front] != nums[next]){
if(next - front > 1){
nums[front + 1] = nums[next];
}
front++;
}
next++;
}
return front + 1;
}
}
(3)合并两个有序数组
oj链接
给你两个按 非递减顺序 排列的整数数组 nums1 和 nums2,另有两个整数 m 和 n ,分别表示 nums1 和 nums2 中的元素数目。
请你 合并 nums2 到 nums1 中,使合并后的数组同样按 非递减顺序 排列。
注意:最终,合并后数组不应由函数返回,而是存储在数组 nums1 中。为了应对这种情况,nums1 的初始长度为 m + n,其中前 m 个元素表示应合并的元素,后 n 个元素为 0 ,应忽略。nums2 的长度为 n 。
从后面开始确定
class Solution {
public void merge(int[] nums1, int m, int[] nums2, int n) {
while(m>0&&n>0) {
if(nums1[m-1]>nums2[n-1]) {
nums1[m+n-1] = nums1[m-1];
m--;
} else {
nums1[m+n-1] = nums2[n-1];
n--;
}
}
while(n>0) {
nums1[n-1] = nums2[n-1];
n--;
}
}
}
链表
链表是一种物理存储结构上非连续的存储结构,数据元素的逻辑顺序是通过链表中的引用链接次序实现的 。
LinkedList的底层是双向链表,由于链表没有将元素存储在连续的空间中,元素存储在单独的节点中,然后通过引用将节点连接起来了,因此任意位置插入或者删除元素时,不需要搬移元素,效率比较高。
LinkedList的构造
LinkedList() | 无参构造 |
---|---|
public LinkedList(Collection<? extends E> c) | 使用其他集合容器中元素构造 |
LinkedList的遍历
import java.util.LinkedList;
import java.util.ListIterator;
public class Test {
public static void main(String[] args) {
LinkedList<Integer> list = new LinkedList<>();
list.add(1); // add(elem): 表示尾插
list.add(2);
list.add(3);
System.out.println(list);
// 使用下标+for遍历
for (int i = 0; i < list.size(); i++) {
System.out.print(list.get(i) + " ");
}
System.out.println();
// foreach遍历
for (int e:list) {
System.out.print(e + " ");
}
System.out.println();
// 使用迭代器遍历---正向遍历
ListIterator<Integer> it = list.listIterator();
while(it.hasNext()){
System.out.print(it.next()+ " ");
}
System.out.println();
// 使用反向迭代器---反向遍历
ListIterator<Integer> rit = list.listIterator(list.size());
while (rit.hasPrevious()){
System.out.print(rit.previous() +" ");
}
System.out.println();
}
}
(1)删除链表中等于给定值 val 的所有节点
oj链接
给你一个链表的头节点 head 和一个整数 val ,请你删除链表中所有满足 Node.val == val 的节点,并返回 新的头节点 。
class Solution {
public ListNode removeElements(ListNode head, int val) {
while(head!=null&&head.val==val) {
head= head.next;
}
if(head == null) {
return null;
}
ListNode prev = head;
while(prev.next!=null) {
if(prev.next.val==val) {
prev.next=prev.next.next;
} else {
prev=prev.next;
}
}
return head;
}
}
(2)反转一个单链表
oj链接
给你单链表的头节点 head ,请你反转链表,并返回反转后的链表。
class Solution {
public ListNode reverseList(ListNode head) {
if(head==null) return null;
ListNode prev = null;
ListNode cur = head;
ListNode next = head.next;
while(next != null) {
cur.next = prev;
prev = cur;
cur = next;
next = next.next;
}
cur.next = prev;
return cur;
}
}
递归
class Solution {
public ListNode reverseList(ListNode head) {
if (head == null || head.next == null) {
return head;
}
ListNode newHead = reverseList(head.next);
head.next.next = head;
head.next = null;
return newHead;
}
}
(3) 合并两个有序链表
oj链接
将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。
class Solution {
public ListNode mergeTwoLists(ListNode l1, ListNode l2) {
ListNode prehead = new ListNode(-1);
ListNode prev = prehead;
while (l1 != null && l2 != null) {
if (l1.val <= l2.val) {
prev.next = l1;
l1 = l1.next;
} else {
prev.next = l2;
l2 = l2.next;
}
prev = prev.next;
}
prev.next = l1 == null ? l2 : l1;
return prehead.next;
}
}
递归
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;
}
}
}
(4)链表的中间结点
oj链接
给你单链表的头结点 head ,请你找出并返回链表的中间结点。
如果有两个中间结点,则返回第二个中间结点。
快慢指针
慢指针走一步,快指针走两步
while循环条件不能换顺序
class Solution {
public ListNode middleNode(ListNode head) {
ListNode fast = head;
ListNode slow = head;
while(fast!=null&&fast.next!=null) {
fast = fast.next.next;
slow = slow.next;
}
return slow;
}
}
(5)链表中倒数第k个结点
牛客网链接
输入一个链表,输出该链表中倒数第k个结点。
public class Solution {
public ListNode FindKthToTail(ListNode head,int k) {
ListNode fast = head;
ListNode slow = head;
if(k<=0||head==null) return null;
while(k-1>0) {
if(fast.next == null) return null;
fast = fast.next;
k--;
}
while(fast.next!= null) {
fast = fast.next;
slow = slow.next;
}
return slow;
}
}
(6)分割链表
oj链接
给你一个链表的头节点 head 和一个特定值 x ,请你对链表进行分隔,使得所有 小于 x 的节点都出现在大于或等于 x 的节点之前。
你不需要 保留 每个分区中各节点的初始相对位置。
class Solution {
public ListNode partition(ListNode head, int x) {
if(head==null) return null;
ListNode less = new ListNode(-1);
ListNode lessHead = less;
ListNode larger = new ListNode(-1);
ListNode largerHead = larger;
while(head != null) {
if(head.val<x) {
less.next = head;
less = less.next;
} else {
larger.next = head;
larger = larger.next;
}
head = head.next;
}
larger.next = null;
less.next = largerHead.next;
return lessHead.next;
}
}
方法二:
class Solution {
public ListNode partition(ListNode head, int x) {
ListNode cur = head;
ListNode bs = null;
ListNode be = null;
ListNode as = null;
ListNode ae = null;
//使用cur来遍历 所有的节点
while (cur != null) {
if (cur.val < x) {
if (bs == null) {
bs = cur;
be = cur;
} else {
be.next = cur;
be = be.next;
}
} else {
// >= x
if (as == null) {
as = cur;
ae = cur;
} else {
ae.next = cur;
ae = ae.next;
}
}
cur = cur.next;
}
if (bs == null) {
return as;
}
be.next = as;
if (as != null) {
ae.next = null;
}
return bs;
}
}
如果bs为null,那么原链表最后一个节点(next是null)是ae,ae的next就不用置为null
(7)输入两个链表,找出它们的第一个公共结点(Y型)
oj链接
给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表不存在相交节点,返回 null 。
图示两个链表在节点 c1 开始相交:
public class Solution {
public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
if (headA == null || headB == null) {
return null;
}
ListNode pA = headA, pB = headB;
while (pA != pB) {
pA = pA == null ? headB : pA.next;
pB = pB == null ? headA : pB.next;
}
return pA;
}
}
(8)给你一个链表的头节点 head ,判断链表中是否有环。
oj链接
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。注意:pos 不作为参数进行传递 。仅仅是为了标识链表的实际情况。
如果链表中存在环 ,则返回 true 。 否则,返回 false 。
思路
快慢指针,即慢指针一次走一步,快指针一次走两步,两个指针从链表起始位置开始运行,如果链表带环则一定会在环中相遇,否则快指针走到链表的末尾。
为什么快指针每次走两步,慢指针走一步可以?
假设链表带环,两个指针最后都会进入环,快指针先进环,慢指针后进环。当慢指针刚进环时,可
能就和快指针相遇了,最差情况下两个指针之间的距离刚好就是环的长度。此时,两个指针每移动
一次,之间的距离就缩小一步,不会出现每次刚好是套圈的情况,因此:在慢指针走到一圈之前,
快指针肯定是可以追上慢指针的,即相遇。
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;
}
}
(9)给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
oj链接
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。
不允许修改 链表。
思路
设链表头部走x步到环的入口, 环有 y 个节点(包括环入口节点)
先跟上一题一样, fast走两步,slow走一步,如果有环的话在环中相遇.
设在环中走了s步(不包括环的入口节点)
那么slow走了x+s步
fast走了x+s+ny(n是圈数)
因为fast走的距离是slow的两倍
所以2(x+s) = x+s+ny
即x=(n-1)*y+(y-s)
让fast从起始位置走,slow从相遇的位置走(n-1)*y+y-s步,他们会在环的入口相遇.
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;
}
}
(10)判定链表是否是回文
即正读和倒读一样的链表
牛客网链接
对于一个链表,请设计一个时间复杂度为O(n),额外空间复杂度为O(1)的算法,判断其是否为回文结构。
给定一个链表的头指针A,请返回一个bool值,代表其是否为回文结构。保证链表长度小于等于900。
思路
先用快慢指针找到中间节点(奇数中间,偶数第二个中间结点),将中间节点之后的节点反转,一头从头结点,一头从末尾结点开始比较.
奇数的情况
当两个指针相遇的时候返回true
偶数的情况
当第一个节点的下一个节点是第二个节点且相等就返回true
public class PalindromeList {
public boolean chkPalindrome(ListNode A) {
if(A==null) {
return true;
}
ListNode fast = A;
ListNode slow = A;
while(fast!=null&&fast.next!=null) {
fast = fast.next.next;
slow = slow.next;
}
ListNode prev = slow;
ListNode cur =slow.next;
while(cur!=null) {
ListNode next = cur.next;
cur.next = prev;
prev = cur;
cur = next;
}
while(A!=slow) {
if(A.val!=prev.val) {
return false;
}
//偶数的情况
if(A.next==prev) {
return true;
}
A=A.next;
prev=prev.next;
}
return true;
}
}
(11)两数相加
class Solution {
public ListNode addTwoNumbers(ListNode l1, ListNode l2) {
ListNode head = new ListNode();
ListNode prev = head;
int t = 0;//记录进位
while(l1 != null || l2 != null || t != 0) {
if(l1 != null) {
t += l1.val;
l1 = l1.next;
}
if(l2 != null){
t += l2.val;
l2 = l2.next;
}
prev.next = new ListNode(t % 10);
prev = prev.next;
t /= 10;
}
return head.next;
}
}
(12)两两交换链表中的节点
- 递归
class Solution {
public ListNode swapPairs(ListNode head) {
if(head == null || head.next == null) return head;
ListNode tmp = swapPairs(head.next.next);
ListNode ret = head.next;
ret.next = head;
head.next = tmp;
return ret;
}
}
- 迭代
class Solution {
public ListNode swapPairs(ListNode head) {
if(head == null || head.next == null) return head;
ListNode newHead = new ListNode();
newHead.next = head;
ListNode prev = newHead, cur = head, next = cur.next, nnext = next.next;
while(cur != null && next != null) {
prev.next = next;
next.next = cur;
cur.next = nnext;
prev = cur;
cur = nnext;
if(cur != null) next = cur.next;
if(next != null) nnext = next.next;
}
return newHead.next;
}
}
(13)重排链表
class Solution {
public void reorderList(ListNode head) {
//处理边界情况
if(head == null || head.next == null || head.next.next == null) return;
//1.找链表的中间节点 - 快慢指针
ListNode slow = head, fast = head;
while(fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
//2.把slow后面的部分逆序
ListNode head2 = null;
ListNode cur = slow.next;
slow.next = null;
while(cur != null) {
ListNode next = cur.next;
cur.next = head2;
head2 = cur;
cur = next;
}
//3.合并两个链表
ListNode cur1 = head, cur2 = head2;
ListNode prev = new ListNode();
while(cur1 != null) {
prev.next = cur1;
prev = cur1;
cur1 = cur1.next;
if(cur2 != null) {
prev.next = cur2;
prev = cur2;
cur2 = cur2.next;
}
}
}
}
(14)合并K个升序链表
- 堆
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
//1.创建小根堆
PriorityQueue<ListNode> heap = new PriorityQueue<>((v1, v2)->(v1.val-v2.val));
//2.把所有的头结点放进小根堆中
for(ListNode head: lists) {
if(head != null) heap.offer(head);
}
//3.合并链表
ListNode head = new ListNode();
ListNode prev = head;
while(!heap.isEmpty()) {
ListNode t = heap.poll();
prev.next = t;
prev = t;
if(t.next != null) heap.offer(t.next);
}
return head.next;
}
}
- 递归
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
return merge(lists, 0 , lists.length - 1);
}
ListNode merge(ListNode[] lists, int left, int right) {
if(left > right) return null;
if(left == right) return lists[left];
//1.平分数组
int mid = (left + right) / 2;
//2.递归处理左右两部分
ListNode l1 = merge(lists, left, mid);
ListNode l2 = merge(lists, mid + 1, right);
//3.合并两个有序链表
return mergeTwoLists(l1, l2);
}
ListNode mergeTwoLists(ListNode l1, ListNode l2) {
if(l1 == null) return l2;
if(l2 == null) return l1;
//合并两个有序链表
ListNode head = new ListNode();
ListNode cur1 = l1, cur2 = l2, prev = head;
while(cur1 != null && cur2 != null) {
if(cur1.val <= cur2.val) {
prev.next = cur1;
prev = cur1;
cur1 = cur1.next;
} else {
prev.next = cur2;
prev = cur2;
cur2 = cur2.next;
}
}
if(cur1 != null) prev.next = cur1;
if(cur2 != null) prev.next = cur2;
return head.next;
}
}
(15)K个一组翻转链表
class Solution {
public ListNode reverseKGroup(ListNode head, int k) {
//1.先求出需要逆序多少组
int n = 0;
ListNode cur = head;
while(cur != null) {
cur = cur.next;
n++;
}
n /= k;
//2.重复n次
ListNode newHead = new ListNode();
ListNode prev = newHead;
cur = head;
for(int i = 0; i < n; i++) {
ListNode tmp = cur;
for(int j = 0; j < k; j++) {
ListNode next = cur.next;
cur.next = prev.next;
prev.next = cur;
cur = next;
}
prev = tmp;
}
//把后面不需要逆序的部分连接上
prev.next = cur;
return newHead.next;
}
}
顺序表和链表的区别
不同点 | 顺序表 | 链表 |
---|---|---|
存储空间上 | 物理上一定连续 | 逻辑上连续,但物理上不一定连续 |
随机访问 | 支持,访问效率O(1) | 不支持,访问效率:O(N) |
头插 | 需要搬移元素,效率低O(N) | 只需修改引用的指向,时间复杂度为O(1) |
容量 | 空间不够时需要扩容 | 没有容量的概念 |
应用场景 | 元素高效存储+频繁访问 | 任意位置插入和删除频繁 |