3.2
设立链表
在链表类中实现这些功能:
-
get(index):获取链表中第 index 个节点的值。如果索引无效,则返回-1。
-
addAtHead(val):在链表的第一个元素之前添加一个值为 val 的节点。插入后,新节点将成为链表的第一个节点。
-
addAtTail(val):将值为 val 的节点追加到链表的最后一个元素。
-
addAtIndex(index,val):在链表中的第 index 个节点之前添加值为 val 的节点。如果 index 等于链表的长度,则该节点将附加到链表的末尾。如果 index 大于链表长度,则不会插入节点。如果index小于0,则在头部插入节点。
-
deleteAtIndex(index):如果索引 index 有效,则删除链表中的第 index 个节点。
删除链表节点:
添加链表节点:
使用虚拟头节点添加头节点
虚拟头节点始终位于链表的最前端,其
next
属性指向真正的头节点(oldHead
)。在这种情况下,插入新节点到链表头部(实际上是虚拟头节点之后)的步骤如下:
创建新节点:创建一个新的节点
newHead
,并设置它的值。链接新节点到当前头节点:将
newHead.next
指向dummy.next
(即oldHead
),这样newHead
就被正确地放置在了虚拟头节点和当前头节点之间。更新虚拟头节点的下一个指针:将
dummy.next
更新为newHead
,这样虚拟头节点现在指向newHead
。
-
链接新节点到当前头节点(实际的第一个数据节点)。
-
更新虚拟头节点,使其指向新插入的节点。
// 假设 newHead 是新创建的节点,dummy 是虚拟头节点 newHead.next = dummy.next; // Step 2: 链接新节点到当前头节点 dummy.next = newHead; // Step 3: 更新虚拟头节点的下一个指针
等价于
ListNode oldHead = dummy.next; // 明确获取当前的头节点 newHead.next = oldHead; // 将新节点链接到oldHead dummy.next = newHead; // 更新虚拟头节点的next指向新节点
创建虚拟头节点
在你的链表数据结构中,初始化一个虚拟头节点并让它的next
指针为null
。这个虚拟头节点将永远不会被删除或更改,它的存在是为了简化操作。
javaCopy codeclass MyLinkedList { private ListNode dummy; // 虚拟头节点 private int size; // 链表元素的个数(不包括虚拟头节点) public MyLinkedList() { dummy = new ListNode(0); // 初始化虚拟头节点,其值通常是不重要的 size = 0; // 初始化链表大小 } }
移动位置
若定义cur为dummy:将cur移动到index位置,需要移动index+1次,也就是for(int i=0;i<=index;i++)
删除节点
若在非头节点位置删除节点
需要先将pre移动到将要删除的位置的前面,然后令pre.next=pre.next.next
单链表
//单链表:单项链接 class ListNode{ int val; ListNode next; //next:指向链表中下一个节点的指针(或链接)。如果当前节点是链表的最后一个节点,则next为null ListNode(){} ListNode(int val){ //要写上定义int val this.val = val; } } //变量 val:这是一个int类型的变量,用于存储节点的值。每个ListNode对象可以保存一个整数值,代表该节点的数据部分。 //变量 next:这是一个ListNode类型的变量,用于指向链表中的下一个节点。如果当前节点是链表的最后一个节点,则此变量通常会被设置为null,表示没有后续节点。 //无参构造函数 ListNode():这是一个默认的构造函数,它不接受任何参数。它允许你创建一个ListNode对象,其val默认为0(如果是基本数据类型的默认值),next默认为null。 //带参构造函数 ListNode(int val):这个构造函数接受一个整数参数val,并创建一个新的ListNode对象,其中val被初始化为传入的参数值,next被默认初始化为null。这允许你在创建节点时直接指定其值。 class MyLinkedList { int size; //size存储链表元素的个数 ListNode dummy; //虚拟头节点:虚拟头节点本身不存储任何实际数据,其next属性指向链表的第一个实际节点。 //初始化链表 public MyLinkedList() { size = 0; dummy = new ListNode(0); } //获取第index个节点的数值,注意index是从0开始的,第0个节点就是头结点 public int get(int index) { //如果index非法,返回-1 if(index<0 || index>=size){ return -1; } ListNode cur = dummy; // index 从0开始,找到对应的index需要dummy向后移动index次 for(int i=0;i<=index;i++){ // int 定义不要忘了 cur = cur.next; } return cur.val; } //在链表最前面插入一个节点,等价于在第0个元素前添加 public void addAtHead(int val) { addAtIndex(0,val); } //在链表的最后插入一个节点,等价于在(末尾+1)个元素前添加;(index从0开始,所以size就是最后一个元素的index+1) public void addAtTail(int val) { addAtIndex(size,val); } // 在第 index 个节点之前插入一个新节点,例如index为0,那么新插入的节点为链表的新头节点。 // 如果 index 等于链表的长度,则说明是新插入的节点为链表的尾结点 // 如果 index 大于链表的长度,则返回空 public void addAtIndex(int index, int val) { if(index > size){ return; } if(index < 0){ index = 0; } size++; //由于将要添加一个新的节点,所以链表的大小size增加1。 ListNode pre = dummy; // 将pred移动到新节点应该插入的位置的前一个节点。由于有虚拟头节点,所以pred开始时指向虚拟头节点,这意味着循环index次后,pred将指向索引index-1的节点。 for(int i=0;i<index;i++){ pre = pre.next; } ListNode toAdd = new ListNode(val); toAdd.next = pre.next; pre.next = toAdd; } public void deleteAtIndex(int index) { // 如果给定的index不在链表的有效范围内,即它是负数或者大于等于链表的当前元素个数size,那么删除操作是无法执行的,因为链表中不存在这样的节点。 if(index>=size || index<0){ return; } size--; // 如果要删除的是索引为0的节点,即链表的第一个实际数据节点(注意,这里的dummy是虚拟头节点,所以dummy.next才是第一个实际数据节点),那么只需要将虚拟头节点的next指针更新为指向下一个节点。更新后,原来的第一个节点就会被垃圾回收器处理,因为没有任何引用指向它。这里直接返回是因为删除操作已完成。 if(index == 0){ dummy.next = dummy.next.next; return; } ListNode pre = dummy; ListNode cur = dummy.next; // 前面已经判断了index=0的情况 for(int i=0;i<index;i++){ // 将pre移动到需要删除的节点的前一个 pre = pre.next; } pre.next = pre.next.next; } } /** * Your MyLinkedList object will be instantiated and called as such: * MyLinkedList obj = new MyLinkedList(); * int param_1 = obj.get(index); * obj.addAtHead(val); * obj.addAtTail(val); * obj.addAtIndex(index,val); * obj.deleteAtIndex(index); */
MyLinkedList linkedList = new MyLinkedList(); linkedList.addAtHead(1); // 链表:1 linkedList.addAtTail(3); // 链表:1->3 linkedList.addAtIndex(1, 2); // 链表:1->2->3 int val = linkedList.get(1); // 返回2 linkedList.deleteAtIndex(1); // 链表:1->3
注意细节
-
for(i=0;...)
时不要忘记定义intfor(int i=0;...)
-
在某个index位置操作时,要注意pre或者dummy要移动到index的位置需要进行多少次
pre=pre.next
-
注意index边界判断,是否返回空或特殊处理
-
要注意如何定义链表
-
-
所有的方法空间复杂度都是 O(1),因为它们只使用了常数个额外的空间。
-
插入和删除操作的时间复杂度为 O(index),因为需要遍历到相应的位置。
-
获取操作的时间复杂度为 O(index),因为需要遍历到对应的节点。
-
双链表
定义虚拟头节点尾节点
public MyLinkedList() { this.size = 0; this.head = new ListNode(0); // 虚拟头结点 this.tail = new ListNode(0); // 虚拟尾结点 head.next = tail; // 初始化时,虚拟头结点直接指向虚拟尾结点 tail.prev = head; // 虚拟尾结点的前驱是虚拟头结点 }
/* 双链表 */ class ListNode{ int val; ListNode next,prev; ListNode(){} ListNode(int val){ this.val = val; } } class MyLinkedList{ int size; ListNode head,tail; public MyLinkedList(){ this.size = 0; this.head = new ListNode(0); // 这里定义了虚拟头节点 this.tail = new ListNode(0); // 这里定义了虚拟尾节点 //这一步非常关键,否则在加入头结点的操作中会出现null.next的错误!!! head.next = tail; tail.prev = head; } public int get(int index){ if(index<0 || index>=size){ return -1; //如果下标无效,则返回 -1 。 } ListNode cur = this.head; // 虚拟头节点 if(index>=size/2){ //从后开始 cur = tail; for(int i=0;i<size-index;i++){ cur = cur.prev; } }else{ //从前开始 for(int i=0;i<=index;i++){ cur = cur.next; } } return cur.val; } public void addAtHead(int val){ addAtIndex(0,val); } public void addAtTail(int val){ //等价于在最后一个元素(null)前添加 addAtIndex(size,val); } public void addAtIndex(int index,int val){ // 将一个值为 val 的节点插入到链表中下标为 index 的节点之前。 // 如果 index 比长度更大,该节点将 不会插入 到链表中。 if(index>size){ return; } if(index<0){ index=0; } size++; ListNode pre = this.head;//虚拟头节点 //pre.next = head; // 这一行是多余的,应该删除:这一行是不正确且多余的,应当从代码中移除。这行代码试图将pre的下一个节点设置为虚拟头节点自身,这显然是错误的。正确的逻辑是创建新节点并将其插入到pre节点和pre.next之间。 for(int i=0; i <index;i++){ // pre移动到需要添加的节点之前的节点 pre = pre.next; } ListNode newNode = new ListNode(val); newNode.next = pre.next; pre.next.prev = newNode; // 双向链,所以两个方向next和prev都要更改 newNode.prev = pre; pre.next = newNode; } public void deleteAtIndex(int index){ // 如果下标有效,则删除链表中下标为 index 的节点。 if(index<0 || index>=size){ return; } size--; ListNode pre = this.head; for(int i=0;i<index;i++){ pre = pre.next; } pre.next.next.prev = pre; pre.next = pre.next.next; } }
注意细节
-
定义双链表时记得 虚拟头节点 虚拟尾节点
-
双向链,所以两个方向next和prev都要更改
-
初始化双链表时:
public MyLinkedList(){ this.size = 0; this.head = new ListNode(0); // 这里定义了虚拟头节点 this.tail = new ListNode(0); // 这里定义了虚拟尾节点 //这一步非常关键,否则在加入头结点的操作中会出现null.next的错误!!! head.next = tail; tail.prev = head; }
-
定义虚拟头节点时不要画蛇添足:
ListNode pre = this.head;//虚拟头节点 //pre.next = head; // 这一行是多余的,应该删除:这一行是不正确且多余的,应当从代码中移除。这行代码试图将pre的下一个节点设置为虚拟头节点自身,这显然是错误的。正确的逻辑是创建新节点并将其插入到pre节点和pre.next之间。 for(int i=0; i <index;i++){ // pre移动到需要添加的节点之前的节点 pre = pre.next; } ListNode newNode = new ListNode(val); newNode.next = pre.next; pre.next.prev = newNode; // 双向链,所以两个方向next和prev都要更改 newNode.prev = pre; pre.next = newNode; }
关于
pre.next = head;
在您的
addAtIndex
方法中,pre.next = head;
这一行的意图似乎是想将虚拟头节点的下一个节点设置为实际的头节点。但实际上,这个操作的上下文是关键:-
在
MyLinkedList
构造函数中,您已经通过head.next = tail;
和tail.prev = head;
建立了虚拟头尾节点之间的初始连接。这时,链表实际上是空的,所以虚拟头节点的下一个节点是虚拟尾节点。 -
当您添加第一个实际数据节点时(无论是通过
addAtHead
还是addAtIndex
),这个节点将会成为虚拟头节点的下一个节点,因此不需要再次显式地设置pre.next = head;
。在添加节点的过程中,您会创建一个新的ListNode
对象,并通过修改指针来插入它,这个过程会自动更新虚拟头节点的next
指向(如果是在头部添加的话)。
-
翻转链表
双指针
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } */ class Solution { public ListNode reverseList(ListNode head) { ListNode pre = null; ListNode cur = head; ListNode temp = null; while(cur != null){ temp = cur.next; // 改变链接结构 cur.next = pre; // 移动位置方便下一个位置的改变 pre = cur; cur = temp; } return pre; } }
递归
基本原理
基本情形:这是递归调用停止的条件。没有基本情形,递归调用会无限进行下去,最终导致堆栈溢出错误。在您的代码示例中,基本情形是
if(cur == null)
,当cur
变为null
时,递归终止。递归情形:在这个阶段,函数将问题分解为更小的子问题,并自我调用来解决这些子问题。在您的例子中,递归调用是
return reverse(cur, temp);
。
// 递归 class Solution { public ListNode reverseList(ListNode head) { return reverse(null,head); } public ListNode reverse(ListNode pre,ListNode cur){ if(cur == null){ return pre; } ListNode temp = null; temp = cur.next; cur.next = pre; return reverse(cur,temp); } }
两两交换链表中的节点
给定一个链表,两两交换其中相邻的节点,并返回交换后的链表。
你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换。
/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode() {} * ListNode(int val) { this.val = val; } * ListNode(int val, ListNode next) { this.val = val; this.next = next; } * } */ class Solution { public ListNode swapPairs(ListNode head) { ListNode dummy = new ListNode(-1); dummy.next = head; ListNode pre = dummy; ListNode temp; // 临时存储第三个节点 ListNode node1; ListNode node2; while(pre.next != null && pre.next.next != null){ temp = pre.next.next.next; node1 = pre.next; node2 = pre.next.next; pre.next = node2; node2.next = node1; node1.next = temp; pre = node1; // 移动,准备下一轮交换 } return dummy.next; // 这个才是真正头节点 } }
-
要注意合法条件及需要交换得两个节点不能为空
-
要临时存储节点
-
要注意交换一次后,当前指针要向后移动
-
-
时间复杂度:遍历整个链表一次,但在遍历过程中,每次交换相邻的两个节点。因此,时间复杂度为 O(n),其中 n 是链表的长度。
-
空间复杂度:除了几个指针变量以外,算法并没有使用额外的空间,因此空间复杂度为 O(1)。
-
删除链表的倒数第N个节点
双指针法
双指针的经典应用,如果要删除倒数第n个节点,让fast移动n步,然后让fast和slow同时移动,直到fast指向链表末尾。删掉slow所指向的节点就可以了。
-
当fast移动到
==null
,也就是最后一个节点时,需要删除的倒数第几个节点就距离fast为n -
保持slow和fast之间的距离为n+1,那么slow就在需要删除的节点的前一个节点
-
先让fast跑出去n+1步
-
fast和slow同时移动直到fast等于null
-
此时的slow.next就是需要删除的节点
-
删除节点
-
返回dummy.next而不是head
当删除的是头节点时:如果要删除的节点正好是链表的头节点(即
head
),那么head
将会改变。因为dummy.next
始终指向当前的头节点,所以在删除操作后,dummy.next
将正确地指向新的头节点,而原始的head
变量仍然指向旧的头节点(已被删除的)。这就是为什么返回dummy.next
而不是head
的原因。
class Solution { public ListNode removeNthFromEnd(ListNode head, int n) { ListNode dummy = new ListNode(-1); dummy.next = head; ListNode fast = dummy; ListNode slow = dummy; for(int i=0; i<=n ;i++){ fast=fast.next; } while(fast!=null){ fast = fast.next; slow = slow.next; } slow.next = slow.next.next; return dummy.next; } }
时间复杂度
O(N):其中N是链表的长度。这个解决方案中,首先通过让
fast
指针先移动n+1
步,然后fast
和slow
指针一起移动直到fast
达到链表末尾,这样整个链表只遍历了一次。因此,时间复杂度是线性的,与链表的长度成正比。空间复杂度
O(1):这个方法在执行过程中只使用了固定的额外空间(
fast
、slow
指针以及几个用于临时存储的变量)。这些额外的空间需求不随输入数据的大小(即链表的长度)而改变,因此空间复杂度是常量级的。
自己的更麻烦的方法
// 计算链表的大小 while (temp != null) { size++; temp = temp.next; }
class Solution { public ListNode removeNthFromEnd(ListNode head, int n) { ListNode dummy = new ListNode(-1); dummy.next = head; int size = 0; // 计算链表的大小 ListNode temp_node = head; while(temp_node != null){ size++; temp_node = temp_node.next; } int move_index = size - n; ListNode pre = dummy; for(int i = 0;i < move_index;i++){ pre = pre.next; } pre.next = pre.next.next; return dummy.next; } }
-
计算size,找到需要删除的节点
-
移动到需要删除的节点之前,删除节点
-
时间复杂度
-
计算链表大小:这一步涉及遍历整个链表一次以计算其大小,时间复杂度为O(N),其中N是链表的节点数。
-
查找要删除节点的前一个节点:这一步涉及再次遍历链表,但这次只遍历到要删除的节点的前一个节点,最坏情况下的时间复杂度也为O(N)。因为在最坏的情况下,要删除的节点可能是链表的最后一个节点,这会导致需要遍历整个链表。
总的来说,因为这两个步骤是顺序执行的,总的时间复杂度是O(N) + O(N) = O(N)。
-
时间复杂度:O(N),其中N是链表的节点数。
-
空间复杂度:O(1),算法执行过程中使用的额外空间是常量的,与输入链表的大小无关。
-
问题所在
while(pre.next != null){ for(int i = 0;i < move_index;i++){ pre = pre.next; } } pre.next = pre.next.next;在这段代码中,您的意图是通过
for
循环移动pre
指针move_index
次来找到要删除的节点的前一个节点。然而,这个逻辑被放在了一个while(pre.next != null)
循环中,这是不必要的,因为for
循环本身已经足够移动pre
指针到正确的位置。更重要的是,while
循环没有更新条件内的任何变量,这会导致一个无限循环,因为条件pre.next != null
始终为真,除非链表为空或只有一个节点。
链表相交
给你两个单链表的头节点 headA 和 headB ,请你找出并返回两个单链表相交的起始节点。如果两个链表没有交点,返回 null 。注意,函数返回结果后,链表必须 保持其原始结构 。
-
简单来说,就是求两个链表交点节点的指针。 这里同学们要注意,交点不是数值相等,而是指针相等。
为了方便举例,假设节点元素数值相等,则节点指针相等。
-
要返回相交节点
public class Solution { public int getLength(ListNode cur){ int len = 0; while(cur != null){ len++; cur = cur.next; } return len; } public ListNode getIntersectionNode(ListNode headA, ListNode headB) { ListNode curA = headA; ListNode curB = headB; int lenA = 0; int lenB = 0; lenA = getLength(curA); lenB = getLength(curB); int move_length = Math.abs(lenA-lenB); ListNode curLong = null; ListNode curShort = null; if(lenA>=lenB){ curLong = headA; curShort = headB; }else if(lenA<lenB){ curLong = headB; curShort = headA; } for(int i=0;i<move_length;i++){ curLong = curLong.next; } while(curLong != null && curShort != null) { if(curLong == curShort) { return curLong; // 当curLong和curShort两个指针在循环中相遇时,表示它们到达了相交的起始节点,这时函数会立即返回当前的节点。 } curLong = curLong.next; curShort = curShort.next; } return null; // 循环结束,未找到相交节点,返回null } }
-
题目是单链表,所以不考虑相交之后分开的问题,就可以假设相交后后面的节点一定相同
-
要把两个链表对齐(后面对齐)
-
比较两节点相等要用
cur == cur
-
总结
-
时间复杂度:O(lenA + lenB),其中
lenA
和lenB
分别是两个链表的长度。 -
空间复杂度:O(1),算法的空间使用量不随输入数据的规模变化。
-
环形链表II
-
慢指针一次移动1个节点,快指针一次移动2个节点,套圈时就不会跳过
-
快指针慢指针在慢指针进入环的第一圈一定相遇
/** * Definition for singly-linked list. * class ListNode { * int val; * ListNode next; * ListNode(int x) { * val = x; * next = null; * } * } */ public class Solution { public ListNode detectCycle(ListNode head) { ListNode fast = head; ListNode slow = head; while(fast != null && fast.next != null){ //fast一次走两个节点所以要判断fast.next也不为空;有效地防止NullPointerException fast = fast.next.next; slow = slow.next; if(fast == slow){ ListNode curHead = head; ListNode curMeet = fast; while(curHead != curMeet){ curHead = curHead.next; curMeet = curMeet.next; } return curHead; } } return null; } }
合并两个有序链表
ListNode mergeTwoLists(ListNode l1, ListNode l2) { ListNode dummy = new ListNode(-1);// 虚拟头节点 ListNode current = dummy; while (l1 != null && l2 != null) { if (l1.val < l2.val) { current.next = l1; l1 = l1.next; } else { current.next = l2; l2 = l2.next; } current = current.next; } current.next = (l1 != null) ? l1 : l2; return dummy.next; }
当退出循环时,意味着至少有一个链表已经被完全遍历完了。此时,另一个链表可能还有剩余的节点没有被添加到新链表中。这行代码:
javaCopy code current.next = (l1 != null) ? l1 : l2;使用了三元运算符来决定应该将哪个链表的剩余部分链接到新链表的末尾:
l1 != null ? l1 : l2
的意思是,如果l1
不为null
(即l1
链表还有剩余的节点未被遍历),则选择l1
,否则选择l2
。
current.next = ...
则是将选中的链表(不为null
的那个)直接链接到当前新链表的末尾。
哈希表
示例
-
当我们遇到了要快速判断一个元素是否出现集合里的时候,就要考虑哈希法。
-
但是哈希法也是牺牲了空间换取了时间,因为我们要使用额外的数组,set或者是map来存放数据,才能实现快速的查找。
-
当我们想使用哈希法来解决问题的时候,我们一般会选择如下三种数据结构。
-
数组
-
set (集合)
-
map(映射)
-
数组
数组是最基础的数据结构之一,可以用于实现简单的哈希表,尤其是当键是整数时。使用数组时,键直接作为数组的索引。这种方法简单且访问速度快,但有几个限制:
-
键的范围应该是有限的,因为数组的大小是固定的。
-
不适用于键的范围很大或键类型不是整数的情况。
示例:使用数组存储字符的频率
javaCopy codeint[] freq = new int[26]; // 假设只处理英文小写字母 // 初始化了一个长度为26的整数数组freq for(char c : "example".toCharArray()) { // 将字符串"example"转换为字符数组,并使用增强型for循环逐个遍历这些字符。每次循环,变量c会被赋予数组中的一个字符 freq[c - 'a']++; // 对于每个字符c,这行代码计算c与'a'之间的差值,用这个差值作为数组freq的索引。这个差值正好对应于字母c在英文字母表中的位置(从0开始计数)。然后,通过++操作符将该位置的计数加1。 // 例如,如果c是'a',则c - 'a'的结果是0,这意味着freq[0]会增加1,表示字母'a'的出现次数加1。 // 如果c是'b',则c - 'a'的结果是1,这意味着freq[1]会增加1,表示字母'b'的出现次数加1。以此类推,对于字符串中的每个字母,都会相应地更新freq数组中的计数。 }
集合(Set)
集合是一种只存储唯一元素的数据结构,它用于表示一组不重复的元素。当你需要快速检查一个元素是否出现在一个集合中,或者需要保证集合中没有重复元素时,使用集合是非常合适的。
-
HashSet
是基于哈希表实现的,提供了高效的插入、删除和查找操作。 -
TreeSet
基于红黑树实现,元素会按照一定的顺序存储,但操作的时间复杂度为O(log n)。
示例:查找数组中是否有重复元素
javaCopy codepublic boolean containsDuplicate(int[] nums) { Set<Integer> set = new HashSet<>(); for(int num : nums) { if(set.contains(num)) { return true; // 找到重复元素 } set.add(num); } return false; }
映射(Map)
映射是存储键值对(key-value pairs)的数据结构,每个键映射到一个唯一的值。映射非常适合于当你需要快速根据键访问值时的场景。
-
HashMap
提供了基于哈希表的实现,支持快速的插入、删除和查找操作。 -
TreeMap
基于红黑树实现,保持了键的顺序,但操作的时间复杂度为O(log n)。
示例:统计字符出现的次数
javaCopy codepublic Map<Character, Integer> countFrequency(String s) { // 在这个HashMap中,键(key)是字符(Character),值(value)是整数(Integer),表示该字符在字符串中出现的次数。 Map<Character, Integer> map = new HashMap<>(); for(char c : s.toCharArray()) { // 将字符串s转换为字符数组,并遍历每个字符。这样可以逐个处理字符串中的每个字符。 map.put(c, map.getOrDefault(c, 0) + 1); // map.getOrDefault(c, 0):首先检查字符 c 是否已经在 map 中有记录。如果有,这个方法返回该字符目前的计数;如果没有,就返回默认值 0。这是为了确保即使 map 中还没有 c 的记录,代码也能正常工作,而不会返回 null 或抛出异常。 // + 1:然后,无论 map.getOrDefault(c, 0) 返回的是实际的计数还是默认值 0,我们都在这个基础上加一。这是因为当前正在处理的字符 c 实际上是在字符串中再次出现,因此需要将其出现次数增加 1。 } return map; }
-
-
-
-
如果键是整数且范围有限,数组是一个简单且有效的选择。
-
如果需要存储一组不重复的元素,集合(set)是更好的选择。
-
如果需要根据键快速访问或更新值,映射(map)是最合适的选项。
-
哈希表是根据关键码的值而直接进行访问的数据结构。
哈希表中关键码就是数组的索引下标,然后通过下标直接访问数组中的元素,如下图所示:
那么哈希表能解决什么问题呢,一般哈希表都是用来快速判断一个元素是否出现集合里。
例如要查询一个名字是否在这所学校里。
要枚举的话时间复杂度是O(n),但如果使用哈希表的话, 只需要O(1)就可以做到。
我们只需要初始化把这所学校里学生的名字都存在哈希表里,在查询的时候通过索引直接就可以知道这位同学在不在这所学校里了。
哈希碰撞
如图所示,小李和小王都映射到了索引下标 1 的位置,这一现象叫做哈希碰撞。
一般哈希碰撞有两种解决方法, 拉链法和线性探测法。
拉链法
刚刚小李和小王在索引1的位置发生了冲突,发生冲突的元素都被存储在链表中。 这样我们就可以通过索引找到小李和小王了
(数据规模是dataSize, 哈希表的大小为tableSize)
其实拉链法就是要选择适当的哈希表的大小,这样既不会因为数组空值而浪费大量内存,也不会因为链表太长而在查找上浪费太多时间。
线性探测法
使用线性探测法,一定要保证tableSize大于dataSize。 我们需要依靠哈希表中的空位来解决碰撞问题。
例如冲突的位置,放了小李,那么就向下找一个空位放置小王的信息。所以要求tableSize一定要大于dataSize ,要不然哈希表上就没有空置的位置来存放 冲突的数据了。如图所示:
有效的字母异位词
-
数组其实就是一个简单哈希表,而且这道题目中字符串只有小写字符,那么就可以定义一个数组,来记录字符串s里字符出现的次数。
-
需要定义一个多大的数组呢,定一个数组叫做record,大小为26 就可以了,初始化为0,因为字符a到字符z的ASCII也是26个连续的数值。
-
需要把字符映射到数组也就是哈希表的索引下标上,因为字符a到字符z的ASCII是26个连续的数值,所以字符a映射为下标0,相应的字符z映射为下标25。
-
再遍历 字符串s的时候,只需要将 s[i] - ‘a’ 所在的元素做+1 操作即可,并不需要记住字符a的ASCII,只要求出一个相对数值就可以了。 这样就将字符串s中字符出现的次数,统计出来了。、
-
那看一下如何检查字符串t中是否出现了这些字符,同样在遍历字符串t的时候,对t中出现的字符映射哈希表索引上的数值再做-1的操作。
-
那么最后检查一下,record数组如果有的元素不为零0,说明字符串s和t一定是谁多了字符或者谁少了字符,return false。最后如果record数组所有元素都为零0,说明字符串s和t是字母异位词,return true。
-
时间复杂度为O(n),空间上因为定义是的一个常量大小的辅助数组,所以空间复杂度为O(1)。
class Solution { public boolean isAnagram(String s, String t) { // 如果两个字符串长度不相等,直接返回false if (s.length() != t.length()) { return false; } // 初始化一个长度为26的数组来记录每个字母的出现次数差异 int[] recorder = new int[26]; // 遍历第一个字符串s,增加每个字母的计数 for(int i = 0; i < s.length(); i++) { recorder[s.charAt(i) - 'a']++; } // 遍历第二个字符串t,减少每个字母的计数 for(int i = 0; i < t.length(); i++) { recorder[t.charAt(i) - 'a']--; } // 检查recorder数组中的每个计数是否都为0 // 如果所有计数都为0,说明s和t是字谜(anagram),否则不是 for(int count : recorder) { if(count != 0) { return false; } } return true; } }
另一种解法
只是把 for(int i = 0; i < s.length(); i++)
变成 for(char c :s.toCharArray())
class Solution { public boolean isAnagram(String s, String t) { // 如果两个字符串长度不相等,直接返回false if (s.length() != t.length()) { return false; } // 初始化一个长度为26的数组来记录每个字母的出现次数差异 int[] recorder = new int[26]; for(char c :s.toCharArray()){ recorder[c - 'a']++; } for(char c:t.toCharArray()){ recorder[c - 'a']--; } // 检查recorder数组中的每个计数是否都为0 // 如果所有计数都为0,说明s和t是字谜(anagram),否则不是 for(int count : recorder) { if(count != 0) { return false; } } return true; } }
注意细节
-
-
s.length()
:这是对字符串s
调用length()
方法的语法,用于获取字符串的长度。在Java中,字符串的长度是通过调用length()
方法来获取的,而不是通过直接访问属性或字段。因此,每当你想要获取一个字符串的长度时,你需要在length
后面加上括号()
,表示这是一个方法调用。 -
s.length
:这种写法尝试直接访问length
属性或字段。但是,在Java的String
类中,length
不是一个可以直接访问的公共属性或字段。实际上,String
类中不存在名为length
的公共属性,这也是为什么尝试编译包含i < s.length
的代码时会发生编译错误。
-
-
定义方法
int[] recorder = new int[26];