数据结构与算法经典问题解析(Java语言描述)——链表相关问题及leetcode中Linkded List Easy题目实现

链表相关问题

问题1:
找到链表的倒数第n个结点
解答:
1)蛮力法:
从链表的第一个结点开始,统计当前结点后面的结点个数。如果后面结点的个数小于n-1,那么算法结束并返回"链表中的结点个数不足"。如果数量大于n-1,则移动到下一个结点(作为新的当前结点)。重复该过程直至当前结点后面的结点个数等于n-1,算法结束。
时间复杂度O(n²),空间复杂度O(1)。
2)使用散列表
新建一个散列表,表中条目是<结点的位置,结点地址>,散列表中每条记录的主键是结点在链表中的位置,值是该结点的地址。创建散列表,当遍历链表时,可以得到链表的长度。令M表示链表的长度,这样寻找链表的倒数第n个结点问题就转换为寻找链表正数第M-n+1个结点。
时间复杂度O(m),空间复杂度O(m)
3) 求链表长度
根据“使用散列表”方法可以得到启发,其本质就是求链表长度,所以从头遍历链表计算出链表的长度,再计算M-n+1,再从头遍历到第M-n+1个结点。这种方法需要遍历两次链表,一次求表长,一次找第M-n+1个结点
时间复杂度T(n) = O(n) + O(n) = O(n),空间复杂度O(1)。
4)一次扫描,两个指针
使用两个指针pNthNode和pTemp。首先,两个指针都指向链表的表头结点。仅当pTemp进行了n次移动后,pNthNode才开始移动。然后两个指针同时移动,直至pTemp到达表尾。这时pNthNode指针所指向的结点就是所求的结点,也就是链表的倒数第n个结点。

ListNode NthNodeFromEnd(ListNode head,int NthNode)
{
		ListNode pTemp = head,pNthNode = null;
		for(int count = 1;count < NthNoe;count++){
		if(pTemp != null)
			pTemp = pTemp.getnext();
		}
		while(pTemp != null){
				if(pNthNode	== null)
					pNthNode = head;
				else
					pNthNode = pNthNode.getNext();
				pTemp = pTemp.getNext();
		}
		if(pNthNode != null)
			return	pNthNode;
		return null;		
}

问题2:
判定给定的链表是以NULL结尾,还是形成一个环。
解答:

1)蛮力法:
带环的链表与常规链表的区别在于,其中有两个节点的后继结点是相同的。在常规链表中是不存在环的,每个结点的后继结点是唯一的。也就是说,若链表中出现后继指针重复,就表明存在环。
最简单直接的判定方式就是,从一个结点开始,令其为当前结点,然后看看链表中其他结点的后继指针是否指向当前结点。如果存在这样的结点,那么就可以判定链表中存在环。否则,对链表中其余结点重复上述过程。
该方法需要判断表尾,否则算法容易出现死循环。
2)使用散列表
算法:

  • 从表头结点开始,逐个遍历链表中的每个结点。
  • 对于每个结点,检查该结点的地址是否存在于散列表中。
  • 如果存在,则表明当前访问的结点已经被访问过了。出现这种情况只能是因为给定链表中存在环。
  • 如果散列表中没有当前结点的地址,那么把该地址插入到散列表中
  • 重复上述过程,直至到达表尾或者找到环。

时间复杂度为O(n),空间复杂度O(n)
3)本问题不能使用排序技术求解
4)Floyd环判定算法
该方法使用快慢指针,若存在环,这两个指针一定会相遇,指向同一位置。

boolean DoesLinkedListContainsLoop(ListNode head){
	if(head == null)
		return false;
	ListNode slowPtr = head,fastPtr = head;
	while(fastPtr.getNext() != null && fastPtr.getNext().getNext() != null){
		slowPtr = slowPtr.getNext();
		fastPtr = fastPtr.getNext().getNext();
		if(slowPtr == fastPtr)
			return true;
	}
	return false;
}			

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

算法应用:
leetcode 141. Linked List Cycle
问题描述:
Given a linked list, determine if it has a cycle in it.
To represent a cycle in the given linked list, we use an integer pos which represents the position (0-indexed) in the linked list where tail connects to. If pos is -1, then there is no cycle in the linked list.

在这里插入图片描述
代码实现:

/**
 * Definition for singly-linked list.
 * class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
public class Solution {
    public boolean hasCycle(ListNode head) {      
        if(head == null){
            return false;
        }
        
        ListNode fastPtr = head;
        ListNode slowPtr = head;
        
        while(fastPtr.next != null&&fastPtr.next.next != null){
            slowPtr = slowPtr.next;
            fastPtr = fastPtr.next.next;
            
            if(slowPtr == fastPtr)
                return true;
                
        }
           return false;
    }
}

另外,在Floyd环判定算法中,如果两个指针每次分别移动2个结点和3个结点,而不是移动1个结点和2个结点,算法仍然有效,但是算法的复杂度可能增加。

问题3:
判定给定的链表是否以NULL结束。如果存在环,找到环的起始结点。
解答:

对上述Floyd环判定算法进行扩展,找到环后,初始化slowPtr的值,使其指向链表表头结点,然后slowPtr和fastPtr从各自的位置开始沿着链表移动,每次均移动一个结点。它们相遇的位置就是环的开始结点。这种方法常用于删除环。

int FindBeginofLoop(ListNode head){
	ListNode slowPtr = head,fastPtr = head;
	boolean loopExists = false;
	if(head == null)
		return false;
	while(fastPtr.getNext() != null && fastPtr.getNext().getNext() != null){
		slowPtr = slowPtr.next;
		fastPtr = fastPtr.getNext().getNext();
		if(slowPtr == fastPtr){
			loopExists = true;
			break;
			}
	}
	if(loopExists){
		slowPtr = head;
		while(slowPtr != fastPtr){
			fastPtr = fastPtr.getNext();
			slowPtr = slowPtr.getNext();
			}
			return slowPtr;//返回环的开始结点
	}
	return null;//环不存在
}				

时间复杂度O(n),空间复杂度为O(1)
问题4:
判定给定的链表是否以NULL结束。如果存在环,返回环的长度。
解答:

本题仍然需要对基本的环判定算法进行拓展。在找到链表中的环后,保持slowPtr指针不变,fastPtr指针则继续移动。每次移动fastPtr指针时,计数器变量也要加1,直至再一次回到slowPtr指针所在的位置,就可以求出环的长度。

int FindLoopLength(ListNode head){
	ListNode slowPtr = head,fastPtr = head;
	boolean loopExists = false;
	int counter = 0;
	if(head == null)
		return 0;
	while(fastPtr.getNext() != null && fastPtr.getNext().getNext() != null){
		slowPtr = slowPtr.next;	
		fastPtr =  fastPtr.getNext().getNext();
		if(slowPtr == fastPtr){
			loopExists = true;
			break;
			}
		}
		if(loopExist){
			fastPtr = fastPtr.getNext();
			while(slowPtr != fastPtr){
				fastptr = fastPtr.getNext();
				counter++;
			}
			return counter;
		}
		return 0;//链表中没有环
	}			

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

问题5:
在有序链表中插入一个结点。
解答:

遍历链表,找到存放元素的正确位置后,插入结点。

ListNode InsertInSortList(ListNode head,ListNode newNode){
	ListNode current = head;
	if(head == null)
		return newNode;
	//遍历链表,直至找到比新结点中数据值更大的结点
	while(current != null && current.getData() < newNode.getData()){
		temp = current
		current = current.getNext();
	}
	//在该结点前插入新结点
	newNode.setNext(current);
	temp.setNext(newNode);
	return head;
}		

时间复杂度O(n),空间复杂度O(1)
问题6:
逆置单向链表
解答:

//迭代版本
ListNode ReverseList(ListNode head){
	ListNode temp = null,nextNode = null;
	while(head != null){
		nextNode = head.getNext();
		head.setNext(temp);
		temp =head;
		head = nextNode();
	}
	return temp;
}		

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

问题7:
假设两个单向链表在某个结点相交后,成为一个单向链表。两个单向链表的表头结点是已知的,但是相交的结点未知。也就是说,它们相交之前各自的结点数是未知的,并且两个链表的结点数也可能是不同。假设链表List1和链表List2在相交前的结点数分别为n和m,那么m可能等于或小于n,也可能大于n。设计算法找到两个链表的合并点。
解答:

1)蛮力法
把第一个链表中的每一个结点指针与第二个链表中的每一个结点指针比较,当出现相等的结点指针时,即为相交结点。但是这种方法的时间复杂度O(mn)较高。
时间复杂度O(mn),空间复杂度O(1)
2)不能使用排序技术来求解这个问题。
3)使用散列表
算法:

  • 选择结点较少的链表(如果链表的长度是未知的,那么随便选择一个链表),将其所有结点的指针值保存在散列表中。
  • 遍历另一个链表,对于该链表中的每一个结点,检查散列表中是否已经保存了其结点的指针。
  • 如果两个链表存在合并点,那么必定会在散列表中找到记录(结点指针)。

时间复杂度为:O(m)+O(n),空间复杂度O(n)或O(m)。
4)使用栈
算法:

  • 创建两个栈:一个用于第一个链表,另一个用于第二个链表。
  • 遍历第一个链表,把所有结点地址压入第一个栈。
  • 遍历第二个链表,把所有结点地址压入第二个栈。
  • 这时,两个栈分别包含了对应链表的结点地址。
  • 比较两个栈的栈顶元素
  • 如果两者相等,那么弹出两个栈的栈顶元素并保存在临时变量中
  • 继续上述过程,直至两个栈的栈顶元素不相等。
  • 这就表示找到了两个链表的合并点。
  • 返回临时变量的值。

时间复杂度O(m+n),空间复杂度O(m+n)
5)使用在数组中“查找第一个重复数”的方法
算法:

  • 创建一个数组A,在数组中保存两个链表中所有结点的后继指针。
  • 在数组A中查找第一个重复元素
  • 第一个重复元素即为两个链表的合并点。

时间复杂度O(m+n),空间复杂度O(m+n)
6)结合排序和搜索,设计复杂度更低的方法
算法:

  • 创建一个数组A,在数组中保存第一个链表中所有的结点的后继指针。
  • 对数组A进行排序。
  • 然后,对于第二个链表中的每一个结点,在排序数组中搜索(使用折半查找)
  • 由于是逐个扫描第二个链表中的结点,所以出现在数组中的第一个重复元素一定是合并点。

时间复杂度O(max(mlogm,nlogn)),空间复杂度O(max(m,n))
7)复杂度更低的方法
算法:

  • 获得两个链表的长度——O(n)+O(m)=O(max(m,n))。
  • 计算两个长度差d——O(1)。
  • 从较长链表的表头开始,移动d步——O(d)。
  • 在两个链表中开始同时移动,直至出现两个后继指针值相等的情况O(min(m,n)).
  • 时间复杂度为O(max(m,n))。
  • 空间复杂度O(1)。
ListNode FindIntersectingNode(ListNode list1,ListNode list2){
	int L1,L2 = 0,diff = 0;
	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;
	}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.getData();
		head1 = head1.getNext();
		head2 = head2.getNext();
	}
	return null;
}							

算法应用:
leetcode 160. Intersection of Two Linked Lists
问题描述:
Write a program to find the node at which the intersection of two singly linked lists begins.

For example, the following two linked lists:
在这里插入图片描述
在这里插入图片描述
代码实现:

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) {
 *         val = x;
 *         next = null;
 *     }
 * }
 */
public class Solution {
   public ListNode getIntersectionNode(ListNode headA, ListNode headB) {
	
        if(headA == null || headB ==null){
            return null;
        }
        
        int lenA = 0,lenB = 0;
		int step = 0;
        ListNode PtrA = headA,PtrB = headB;
		
        while(PtrA != null){
            lenA++;
            PtrA = PtrA.next;
        }
        
        while(PtrB != null){
            lenB++;
            PtrB = PtrB.next;
        }
		PtrA = headA;
		PtrB = headB;
		
        if(lenA >= lenB){
            step = lenA - lenB;
            while(step>0){
                PtrA = PtrA.next;
                step--;
            }
			while(PtrA!=PtrB)
            {
                if(PtrA==null||PtrB==null) return null;
                PtrA = PtrA.next;
                PtrB = PtrB.next;
            }
            
            return PtrA;
        }else{
            step = lenB - lenA; 
            while(step> 0){
                PtrB = PtrB.next;
                step--;
            }
			
			while(PtrA != PtrB){
            if(headA==null || headB==null) return null;
            PtrB = PtrB.next;
            PtrA = PtrA.next;
        }
		        return PtrA;
        }   
    }
}

问题8:
如何找到链表的中间结点
解答:

1)蛮力法
在链表中对每一个结点统计其后结点的个数,然后判定其是否为中间结点。
时间复杂度O(n²),空间复杂度O(1)
2)求表长
算法:

  • 遍历链表,得到链表的长度n
  • 从表头开始再次扫描链表,定位到第n/2个结点,即为中间结点。

时间复杂度O(n),空间复杂度O(1)
3)使用散列表
4)使用一次扫描
可以使用两个指针,让第一个指针的移动速度是另一个的2倍,当第一个到达表尾的时候,另一个指针则指向中间结点。

 ListNode FindMiddle(ListNode head){
 	ListNode ptr1x,ptr2x;
 	ptr1x = ptr2x = head;
 	int i = 0;
 	int count = 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;
 	}
 	count++;
 }
	if(count % 2 != 0) return ptr2x.next;
	else return ptr2x;
} 		

时间复杂度O(n),空间复杂度O(1)
算法应用:
leetcode 876. Middle of the Linked List
问题描述:
Given a non-empty, singly linked list with head node head, return a middle node of linked list.
If there are two middle nodes, return the second middle node.
在这里插入图片描述
解答:

/**
 * Definition for singly-linked list.
 * public class ListNode {
 *     int val;
 *     ListNode next;
 *     ListNode(int x) { val = x; }
 * }
 */
class Solution {
    public ListNode middleNode(ListNode head) {
        ListNode Ptr1x = head,Ptr2x = head;
        
        int i = 0;
        int count = 0;
        while(Ptr1x.next != null){
            if(i == 0){
                Ptr1x = Ptr1x.next;
                i = 1;
            }else if(i == 1){
                Ptr1x = Ptr1x.next;
                Ptr2x = Ptr2x.next;
                i = 0;
            }
            count++;
        }
        if(count % 2 != 0)
            return Ptr2x.next;
        else
            return Ptr2x;
        
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值