链表: 基本操作
线性表的顺序存储结构的特点是逻辑关系上相邻的两个元素在物理位置上也相邻,因比随机存取元素时比较简单,但是这个特点也使得在插入和删除元素时,造成大量的数据元素移动 ,同时如果使用静态分配存储单元 ,还要预先占用连续的存储空间 ,可能造成空间的浪费或空间的溢出。如果采用链式存储,就不要求逻辑上相邻的数据元素在物理位置上也相邻,因此它没有顺序存储结构所具有的缺点,但同时也失去了可随机存取的优点。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 private Node head;private int N;private class Node { T item; Node next; public Node (T item, Node next) { this .item = item; this .next = next; } }
初始化头结点: 1 2 3 4 5 6 7 8 public LinkList () { this .head = new Node(null , null ); this .N = 0 ; }
获取指定元素: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public T get (int i) { if (i > N || i < 0 ) { throw new IllegalArgumentException("index is error" ); } else { int k = 0 ; Node node = head; while (k < i && node != null ) { node = node.next; k++; } Node crrNode = node.next; return crrNode.item; } }
尾插法:
该方法从一个空表开始依次读取数组a中的元素,生成一个新结点s,将读取的数组元素存放到该结点的数据域中,然后将建插人到当前链表的表尾上,如图2.18所示,直到数组a的所有元素读完为止。为此需要增加一个尾指针r,使其始终指向当前链表的尾结点,每插人一个新结点后让:指向这个新结点最后还需要将r所指结点(尾结点)的next域置为空。采用尾插法建表的算法如下:
r->next =s
r=s
删除一个结点:
q=p->next;//q临时保存被删除的结点
p->next=q->next //从链表删除结点q
或者
p->next =p->next->next
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public T remove (int n) { if (n < 0 || n > N) { throw new IllegalArgumentException("index error" ); } else { Node pre = head; for (int i = 0 ; i < n - 1 ; i++) { pre = pre.next; } Node deleNode = pre.next; pre.next = deleNode.next; N--; return deleNode.item; } }
插入结点 这里插人指在单链表的两个数据域分别为α和b的结点(已知a结点的指针p)之间插人一个数据域为α的结点(由s指向它).加图2.14(a)所示。其操作是首先让x结点的指针域(s一>next)指向(b结点( p一>next) 然后a结点的指针域O(p一> next)指向x结点(s),从而实现3个结点之间逻辑兰系的亦化.插从特程如图2.14(b)和(c)所示,图2.14(d)是插入后的结果。
s->next=p->next
p->next =s
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 public void insert (T t, int n) { if (n < 0 ) { throw new IllegalArgumentException("index is error" ); } else { Node pre = head; for (int i = 0 ; i < n - 1 ; i++) { pre = pre.next; } Node indexNode = new Node(t, pre); pre.next = indexNode; N++; } } public void insert (T t) { Node node = head; while (node.next != null ) { node = node.next; } Node newNode = new Node(t, null ); node.next = newNode; N++; }
快慢指针解决环路:
给定一个链表,判断链表中是否有环。
问题分析
如果没有环,快指针将停在链表的末尾
如果有环,那么最后就会相遇
快慢指针速度问题
每次移动慢指针一步,而移动快指针移动两步
方法一: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public boolean hasCycle (ListNode head) { ListNode fast = head; ListNode slow = head; if (head == null ) { return false ; } while (fast.next != null && fast.next.next != null ) { fast = fast.next.next; slow = slow.next; if (slow == fast) { return true ; } } return false ; }
假如有环,那么快慢指针最终都会走到环上 ,假如环的长度是m,快慢指针最近的间距是n,如下图中所示
快指针每次走两步,慢指针每次走一步,所以每走一次快慢指针的间距就要缩小一步,在图一中当走n次的时候就会相遇,在图二中当走m-n次的时候就会相遇。
复杂度分析 时间复杂度:
O(N),其中 N是链表中的节点数。
当链表中不存在环时,快指针将先于慢指针到达链表尾部,链表中每个节点至多被访问两次。
当链表中存在环时,每一轮移动后,快慢指针的距离将减小一。而初始距离为环的长度,因此至多移动 N轮。
空间复杂度:
O(1) 我们只使用了两个指针的额外空间。
方法二: 存放至set集合中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 public boolean hasCycleBySet (ListNode head) { if (head == null ) { return false ; } ListNode pre = head; Set<ListNode> isCycle = new HashSet<>(); while (pre.next != null ) { pre= pre.next; if (!isCycle.add(pre)) { return true ; } } return false ; }
时间复杂度:O(N),其中 N是链表中的节点数。最坏情况下我们需要遍历每个节点一次。
空间复杂度:O(N),其中 N 是链表中的节点数。主要为哈希表的开销,最坏情况下我们需要将每个节点插入到哈希表中一次。
环形链表 II
给定一个链表的头节点 head ,返回链表开始入环的第一个节点。 如果链表无环,则返回 null。
如果链表中有某个节点,可以通过连续跟踪 next 指针再次到达,则链表中存在环。 为了表示给定链表中的环,评测系统内部使用整数 pos 来表示链表尾连接到链表中的位置(索引从 0 开始)。如果 pos 是 -1,则在该链表中没有环。注意:pos 不作为参数进行传递,仅仅是为了标识链表的实际情况。
不允许修改 链表。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public ListNode detectCycle (ListNode head) { if (head==null ){ return null ; } if (head!=null &&head.next==null ){ return null ; } ListNode pre =head; Set<ListNode> list= new HashSet<>(); while (pre!=null &&pre.next!=null ){ if (!list.add(pre)){ return pre; } pre =pre.next; } return null ; }
Hash 表方法: 先将链表 A 放入 Map 中,然后依次取出判断是否含有该值,如果第一个有的话就是代表有该值为链表相交结点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 public class Solution { public ListNode getIntersectionNode (ListNode headA, ListNode headB) { ListNode first = headA; ListNode second = headB; HashMap<ListNode,ListNode> map = new HashMap<>(); if (headA==null ||headA==null ){ return null ; } while (first!=null ){ map.put(first,first); first= first.next; } while (second!=null ){ if (map.get(second)!=null ){ return second; } second =second.next; } return null ; } }
方法二(双指针法):
一种比较巧妙的方式是,分别为链表 A 和链表 B 设置指针 A 和指针 B,然后开始遍历链表,如果遍历完当前链表,则将指针指向另外一个链表的头部继续遍历,直至两个指针相遇。 最终两个指针分别走过的路径为: 指针 A :a+c+b 指针 B :b+c+a 明显 a+c+b = b+c+a, 因而如果两个链表相交,则指针 A 和指针 B 必定在相交结点相遇。
时间复杂度为 o (m+n), 其中 m 和 n 分别为两个指针的长度,
空间复杂度为 o (1)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 public ListNode getIntersectionNode02 (ListNode headA, ListNode headB) { if (headA==null ||headB==null ){ return null ; } 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; }
合并两个有序链表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 class Solution { public ListNode mergeTwoLists (ListNode list1, ListNode list2) { ListNode dummy = new ListNode(-1 ); ListNode p = dummy; ListNode pA = list1; ListNode pB = list2; while (pA!=null &&pB!=null ){ if (pA.val>pB.val){ p.next= pB; pB= pB.next; }else { p.next=pA; pA=pA.next; } p=p.next; } if (pA != null ) { p.next = pA; } if (pB!= null ) { p.next = pB; } return dummy.next; } }
递归魔法:反转单链表 :: labuladong 的算法小抄 (gitee.io)
头插法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 class Solution { public ListNode reverseList (ListNode head) { ListNode dummy = new ListNode(0 ); ListNode temp ; ListNode pre; if (head==null ){ return head; } while (head!=null ){ temp = new ListNode(head.val,head); pre = dummy.next; dummy.next = temp; temp.next= pre; head = head.next; } return dummy.next; } }
使用递归进行反转 1 2 3 4 5 6 7 8 9 10 11 public ListNode reverseList (ListNode head) { if (head == null || head.next == null ) { return head; } ListNode p = reverseList(head.next); head.next.next = head; head.next = null ; return p; } }
reverse(head.next)
执行完成后,整个链表就成了这样:
并且根据函数定义,reverse
函数会返回反转之后的头结点,我们用变量 last
接收了。
1 2 3 head.next = null ; return last;
1 2 3 4 if (head == null || head.next == null ) { return head; }
意思是如果链表为空或者只有一个节点的时候,反转结果就是它自己,直接返回即可。
2、当链表递归反转之后,新的头结点是 last
,而之前的 head
变成了最后一个节点,别忘了链表的末尾要指向 null:
head.next = null ;