文章目录
当你找不到思路的时候,想想有哪些数据结构
比如说:
常用的数据结构:数组、链表、队、栈、Hash、集合、树、堆。
常用的算法思想:查找、排序、双指针、递归、迭代、分治、贪心、回溯、动态规划。
1.两个链表第一个公共子节点
链表结构
class ListNode(){
public int val;
public ListNode next;
ListNode (int x){
val = x;
next = null;
}
}
题目描述:输入两个链表,找出它们的第一个公共子节点。两个链表的头结点都是已知的,相交之后成为一个单链表,但是相交的位置未知,并且相交之前的结点数也是未知的,请设计算法找到两个链表的合并点。
图:
我的分析:
1.蛮力法,双重循环,两个指针,一个个比对,如果相等就输出该结点的位置或者值。
2.用Map存第一个链表的结点,进行比较,和蛮力法区别在于时间复杂度是o1,哈希能用集合也行。
我的代码:On²,P常数级
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) {
* val = x;
* next = null;
* }
* }
*/
class Solution {
ListNode getIntersectionNode(ListNode headA, ListNode headB) {
ListNode p1 = headA;
ListNode p2 = headB;
while(p1!=null)
{
p2=headB;
while(p2!=null)
{
if(p1==p2)
return p2;
else {
p2=p2.next;
}
}
p1=p1.next;
}
return null;
}
}
栈和集合方法:
栈:虽然两个链表长度不等,但是后面的结点是一致的,所以根据栈先进后出的特点,将两个链表压入两个栈中,先出的都是一致的结点,最晚出的就是第一个公共子节点。
代码:
这里用到了java中的栈和泛型,因为结点类型不是固定的。
class Solution {
ListNode getIntersectionNode(ListNode headA, ListNode headB) {
//Creat New Stack
Stack<ListNode> stackA = new Stack();
Stack<ListNode> stackB = new Stack();
//push Element
while(headA!=null){
stackA.push(headA);
headA=headA.next;
}
while(headB!=null){
stackB.push(headB);
headB=headB.next;
}
//pop Element and compare
//循环结束条件,存在栈为空
ListNode temp = null;
while(stackA.size()>0 && stackB.size()>0)
{
if(stackA.peek()==stackB.peek()){
temp = stackA.pop();
stackB.pop();
}else{
break;
}
}
return temp;
}
}
2.判断是否为回文序列
回文序列:一个序列是对称的,有偶数个元素。
我的分析:
1.用数组从中间向两边查找
2.因为是对称的,所以只要翻转过来,每一个对应的值还是相等的就是回文序列。
我的代码:时间复杂度 o (3n) 空间复杂度
class Solution {
public boolean isPalindrome(ListNode head) {
ListNode preNode = head;
Stack <ListNode> stack = new Stack();
// count n
while(preNode!=null)
{
stack.push(preNode);
preNode = preNode.next;
}
/*
if(stack.size()%2==1)
{
return false;
}
*/
// count 2n
while(stack.size()>0 && head!=null)
{
if(stack.pop().val!=head.val)
return false;
head=head.next;
}
return true;
}
}
总结:想不出来,抄。
-
方法一:将链表元素赋值到数组中,从数组两端向中间对比。但是会被视为逃避链表。
-
方法二:如上。先压栈,然后遍历比较。
-
方法三:优化2,压栈得到总长度,遍历一半。省了 三分之一时间
-
方法四:反转链表,先创建一个链表newList,将原始链表OldList 元素值 逆序保存到newList中,然后重新遍历。。。我的思考:这种方法对于我这样一个新手来说问题在于链表是顺序的,如何将最后一个元素到第一个元素逆序放到新链表中是个问题。但其实很简单,旧链表肯定是不可逆的顺序查找,但是查找过程中,把当前结点作为头结点插到新链表中就可以了。
-
方法五:优化4,反转一半元素。
-
方法六:优化5,使用双指针思想中的快慢指针,fast一次走两步,slow一次走一步。当fast到达表尾的时候,slow正好到达一半的位置,那么接下来可以从头开始逆序一半的元素,或者从slow开始逆序一半。
-
方法七:在遍历的时候使用递归来反转一半的链表。
-
方法八:如果使用方法六的话,不需要逆序一半的元素,可以直接将fast指向头结点,low指向一半节点的下一个节点然后判断。就不需要逆序的时间复杂度。但是这个方法虽然简单却只能考虑到回文序列是合法的情况。如果回文序列不合法,只有一个两个元素,或者元素不足偶数位就不行。
实例代码:快慢指针+反转,方法六。
class Solution {
public boolean isPalindrome(ListNode head) {
ListNode fast = head,low = head;
while(fast != null && fast.next != null)
{
fast = fast.next.next;
low = low.next;
}
fast = head;
low = reBack(low);
while(low!=null && low.val == fast.val)
{
low = low.next;
fast = fast.next;
}
return low == null ? true:false;
}
public ListNode reBack(ListNode head){
ListNode pre = null,next = null;
while(head != null){
next = head.next;
head.next = pre;
pre = head;
head = next;
}
return pre;
}
}
3.合并有序链表
合并有序链表有三种情况,都为空,都不为空,有为空的链表。
3.1 合并两个有序链表
介绍:将两个升序链表合并为一个新的升序链表并返回,新链表是通过拼接给定的两个链表的所有结点组成的。
我的思路:分为三种情况,链表都为空。部分为空。全不为空。
然后将其中一个链表节点接到另一个链表相应位置上,但是最简单的是直接接到新链表上。
代码:On O2n
class Solution {
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
ListNode newhead = new ListNode();
ListNode newcur = newhead;
if(list2 == null)
{
return list1;
}
else if(list1==null )
{
return list2;
}
else {
while(list1!=null && list2!=null)
{
if(list1.val<list2.val)
{
ListNode temp = list1.next;
newcur.next =list1;
list1.next = null;
newcur = newcur.next;
list1 = temp;
}else{
ListNode temp = list2.next;
newcur.next =list2;
list2.next = null;
newcur = newcur.next;
list2 = temp;
}
}
if(list1 == null && list2 == null) //if(list1.equals(list2))
return newhead.next;
else if (list1==null)
{
newcur.next = list2;
return newhead.next;
}else{
newcur.next = list1;
return newhead.next;
}
}
}
}
拓展优化代码:
我的思考:观看上述代码块,不难发现有两个地方有点臃肿,第一,判断链表空的情况可以直接在循环下面写。第二,循环下面的链表连接和第一是重复的可以合并
再看循环体,循环体也很臃肿。我们重新看一遍合并链表的过程。其实是用新链表当前头结点直接去连接小的那个头结点,没必要保存后继而且指空,就直接接到那个表上,然后表头结点下移。
class Solution {
public ListNode mergeTwoLists(ListNode list1, ListNode list2) {
ListNode newhead = new ListNode();
ListNode newcur = newhead;
while(list1!=null && list2!=null)
{
if(list1.val<list2.val)
{
newcur.next =list1;
list1 =list1.next;
newcur = newcur.next;
}else{
newcur.next =list2;
newcur = newcur.next;
list2 = list2.next;
}
}
newcur.next = list1 == null ? list2 : list1;
return newhead.next;
}
}
运行完这段代码后,虽然变得简洁了,但是内存用的多了。
3.2 合并 K 个升序链表
代码:时间复杂度偏高
class Solution {
public ListNode mergeKLists(ListNode[] lists) {
ListNode cur = null;
for(ListNode list : lists)
{
cur = mergeTwoLists(cur , list);
}
return cur;
}
ListNode mergeTwoLists(ListNode list1 , ListNode list2)
{
ListNode newhead = new ListNode();
ListNode newcur = newhead;
while(list1!=null && list2!=null)
{
if(list1.val<list2.val)
{
newcur.next =list1;
list1 =list1.next;
newcur = newcur.next;
}else{
newcur.next =list2;
newcur = newcur.next;
list2 = list2.next;
}
}
newcur.next = list1 == null ? list2 : list1;
return newhead.next;
}
}
优化:用归并和堆
3.3 合并两个链表
给你两个链表 list1 和 list2 ,它们包含的元素分别为 n 个和 m 个。
请你将 list1 中下标从 a 到 b 的全部节点都删除,并将list2 接在被删除节点的位置。
代码:
class Solution {
public ListNode mergeInBetween(ListNode list1, int a, int b, ListNode list2) {
ListNode lista=list1,listb=list1;
int i=0,j=0;
while(lista.next!=null)
{
if(i<a-1)
{
i++;
lista=lista.next;
}
if(j<b+1)
{
j++;
listb=listb.next;
}else
break;
}
lista.next = list2;
while(list2.next!=null)
list2=list2.next;
list2.next=listb;
return list1;
}
}
4.双指针专题
4.1 寻找中间结点
给你单链表的头结点 head ,请你找出并返回链表的中间结点。
如果有两个中间结点,则返回第二个中间结点。
我的思考:蛮力法:就是能遍历到中间节点并且返回。双指针,这里用快慢指针,快指针停下的时候,如果下一个结点不为空,就返回慢指针下一个结点。如果为空,就返回慢指针。
代码:
class Solution {
public ListNode middleNode(ListNode head) {
ListNode fast=head ;
ListNode low = head;
while(fast.next!=null && fast.next.next!=null)
{
fast=fast.next.next;
low = low.next;
}
if(fast.next==null)
return low;
else
return low.next;
}
}
4.2 寻找倒数第K个元素
我的思考:快指针走两步,慢指针走一步肯定不行,必须让这两个指针间隔 k ,所以快指针先走,满指针后走。
代码:
class Solution {
public int kthToLast(ListNode head, int k) {
ListNode fast=head , slow =head;
for(int i=1;i<k;i++)
{
fast = fast.next;
}
while(fast.next!=null)
{
fast = fast.next;
slow = slow.next;
}
return slow.val;
}
}
总结:这里的核心思想就是利用快慢指针的相对位置确定慢指针的位置
4.3 旋转链表
法1 整体
给你一个链表的头节点 head ,旋转链表,将链表每个节点向右移动 k 个位置。
我的思考:题目就是将尾结点插到头结点插K次,或者是将末尾K个结点接到表头。
结合上一条题目就是将倒数第k+1个结点指向null,然后尾结点指向头结点。
打完代码测试不通过,原因:如果移动节点超过了链表长度,就会出问题。但是可以把这种情况独立出来。
class Solution {
public ListNode rotateRight(ListNode head, int k) {
if(head==null || head.next==null || k==0)
{
return head;
}
ListNode temp=head;
int len = 0;
while(temp!=null)
{
temp=temp.next;
len++;
}
k= k% len;
if(k==0) return head;
ListNode fast = head,slow = head;
for(int i=1;i<k;i++)
{
fast=fast.next;
}
while(fast.next!=null)
{
fast=fast.next;
if(fast.next==null)
break;
slow=slow.next;
}
ListNode res = slow.next;
slow.next =null;
fast.next=head;
return res;
}
}
法2 一个个插
这样的话,把尾结点插到头结点k次是个比较不错的暴力方法。但是时间复杂度一般比较高。不过结点为空或者只有一个节点,或者循环次数过多就会超时。
class Solution {
public ListNode rotateRight(ListNode head, int k) {
if(head==null||head.next==null)
{
return head;
}
else
{
for(int i=0;i<k;i++)
{
ListNode end = findEnd(head);
if(end!=null){
ListNode temp = end.next;
end.next = null;
temp.next = head;
head = temp;
}
}
return head;
}
}
public static ListNode findEnd(ListNode head)
{
ListNode cur=head;
while(cur!=null&&cur.next!=null&&cur.next.next!=null)
{
cur = cur.next;
}
return cur;
}
}
问题:超时了
改进的代码:
class Solution {
public ListNode rotateRight(ListNode head, int k) {
if(head==null||head.next==null)
{
return head;
}
else
{
k = k % getLen(head);
for(int i=0;i<k;i++)
{
ListNode end = findEnd(head);
if(end!=null){
ListNode temp = end.next;
end.next = null;
temp.next = head;
head = temp;
}
}
return head;
}
}
public static ListNode findEnd(ListNode head)
{
ListNode cur=head;
while(cur!=null&&cur.next!=null&&cur.next.next!=null)
{
cur = cur.next;
}
return cur;
}
public static int getLen(ListNode head)
{
ListNode temp = head;
int len=0;
while(temp!=null)
{
temp=temp.next;
len++;
}
return len;
}
}

被折叠的 条评论
为什么被折叠?



