链表面试题--coding

目录

1、单链表翻转

 2、只给定链表中需要删除的节点node,删除这个节点。

3、判断链表是否有环,如果有,找到环的入口位置

3.1、如果链表有环,寻找环入口位置

3.2 、计算环的长度

4、判断链表是否相交,并且求出第一个交点

5、两个有序链表合并

6、链表去重

7 、链表中倒数第 K 个节点

8、链表快速排序


1、单链表翻转

typedef struct node{
    int value;
    struct node* next;
}node;

void reverse(node* head)  
{  
    if ( (head == NULL) || (head->next == NULL) ) 
        return;// 边界检测  

    node* pPrev = NULL;
    node* pCur = head;  // 保存链表头节点
    while (pCur != NULL)  
    {  
        node* pNext = pCur->next;  // 暂时将下一个节点保存下来,  
        pCur->next = pPrev;        // 将当前节点的  Next 指向前一个节点  
        pPrev = pCur;              // 此时 当前节点 设置成上一节点 
        pCur = pNext;              // 将保存的 pNext 设置为当前节点 
    }  
    return  head = pPrev;  //while 循环跳出条件是pCur==null,以返回了pPrev作为新的头指针
} 
//while循环版
void reverse(node*& head)  
{  
    if ( (head == NULL) || (head->next == NULL) ) return;// 边界检测  
    node* pPrev = head;// 保存链表头节点  
    node* pCur = head->next;// 获取当前节点  
    node* pNext = NULL;  // 下一个结点
    while (pCur != NULL)  
    {  
        pNext = pCur->next;// 将下一个节点保存下来  
        pCur->next = pPrev;// 将当前节点的下一节点置为前节点  
        pPrev = pCur;// 将当前节点保存为前一节点  
        pCur = pNext;// 将当前节点置为下一节点  
    }
   // 重新设置头结点指向  
    head->next = NULL;  
    head = pPrev;  
} 

//递归版
LIST *reverseList2(LIST *phead)
{
    LIST *current = phead;

    //当检测到链表为空,或者已经检测到尾节点时,满足终止条件,停止向下递归
    if(NULL == current || NULL == current->next){
        return current;
    }

    //执行递归操作,寻找尾节点
    LIST *newhead = reverseList2(current->next);

    //反转操作,目的是完成当前节点的下一个节点,完成指向当前节点的操作
    current->next->next = current;
    //该语句很重要!
    current->next = NULL;
    printf("Reverse2 done!\n");
    return newhead;
}

递归方法

先反转后面的链表,从最后面的两个结点开始反转,依次向前,将后一个链表结点指向前一个结点,注意每次反转后要将原链表中前一个结点的指针域置空,表示将原链表中前一个结点指向后一个结点的指向关系断开。

图解单链表反转 - Luego - 博客园

单链表翻转:https://www.jianshu.com/p/84117123f709

                      https://www.cnblogs.com/csbdong/p/5674990.html

LeetCode :力扣

 2、只给定链表中需要删除的节点node,删除这个节点

注意:前提不知道头结点,没法遍历

LeetCode上原题:力扣

分析:如果有头结点,对时间复杂度没有要求,直接遍历节点就可以,时间复杂度为O(n)

要求时间复杂度为O(1),此种情况下只有一种情况不能达到O(1)。

链表删除,可以考虑不单单删除节点,而是修改需要节点值为下一个节点的值,同时把需要删除节点的next指向下下个节点。

下面就是代码,代码就是这么简单

//node 要删除的结点,不知道头结点
public void deleteNode(ListNode node) {
    node.val = node.next.val;    //将node结点下一个结点的值赋值给node.val
    node.next = node.next.next;  //将node结点的下一个结点的next指向地址赋给 node.next
}

方法:node.value --- 需要删除的节点值

           node.next ---需要删除节点的下一个节点

           del.next.value  --下一个节点的值

           del.next.next  -- 下一个结点指向的下一个结点 

   特殊情况是:del节点本身就是最后一个节点,这样的话 del.next = NULL,这种方法没法删除

3、判断链表是否有环,如果有,找到环的入口位置

判断一个链表是否有环,空间复杂度是O(1)

如果不考虑空间复杂度,可以使用一个map记录走过的节点,当遇到第一个在map中存在的节点时,就说明回到了出发点,即链表有环,同时也找到了环的入口。

不适用额外内存空间的技巧是使用快慢指针,即采用两个指针walker和runner,walker每次移动一步而runner每次移动两步。当walker和runner第一次相遇时,证明链表有环
 

以图片为例,假设环的长度为 R,当慢指针walker走到环入口时快指针runner的位置如图,且二者之间的距离为S。在慢指针进入环后的t时间内,快指针从距离环入口 处走了2t个节点,相当于从环入口走了S+2t个节点。而此时慢指针从环入口走了t个节点。

假设快慢指针一定可以相遇,那么有 S+2t−t=nR,即 S+t=nR,如果对于任意的 S,R,n,总可以找到一个t满足上式,那么就可以说明快慢指针一定可以相遇,满足假设(显然可以找到)

而实际上,由于 S<R,所以在慢指针走过一圈之前就可以相遇

所以如果链表中有环,那么当慢指针进入到环时,在未来的某一时刻,快慢指针一定可以相遇,通过这个也就可以判断链表是否有环。

代码如下

/**
 * Definition for singly-linked list.
 * struct ListNode {
 *     int val;
 *     ListNode *next;
 * };
 */

//使用快慢指针
public boolean hasCycle(ListNode head) {
    //边界条件判断
    if (head == null || head.next == null) {
        return false;
    }
    ListNode slow = head;
    ListNode fast = head.next;
    while (slow != fast) {
        if (fast == null || fast.next == null) {
            return false;
        }
        slow = slow.next;
        fast = fast.next.next;
    }
    return true;
}

// 将链表的指针放入set中,每次加入时都去set中查询
public boolean hasCycle(ListNode head) {
    Set<ListNode> nodesSeen = new HashSet<>();
    while (head != null) {
        if (nodesSeen.contains(head)) {
            return true;
        } else {
            nodesSeen.add(head);
        }
        head = head.next;
    }
    return false;
}

参考:LeetCode:力扣

3.1、如果链表有环,寻找环入口位置

上一个问题得到结论:慢指针走过一圈之前就可以相遇

以图片为例,假设环入口距离链表头的长度为L,快慢指针相遇的位置为 cross,且该位置距离环入口的长度为S。考虑快慢指针移动的距离,慢指针走了 L+S,快指针走了L+S+nR(这是假设相遇之前快指针已经绕环n圈)。由于快指针的速度是慢指针的两倍,相同时间下快指针走过的路程就是慢指针的两倍,所以有2(L+S)=L+S+nR,化简得L+S=nR

n=1时,即快指针在相遇之前多走了一圈,即L+S=R,也就是L=R−S,观察图片,L表示从链表头到环入口的距离,而R−S表示从cross继续移动到环入口的距离,既然二者是相等的,那么如果采用两个指针,一个从表头出发,一个从cross出发,那么它们将同时到达环入口。即二者相等时便是环入口节点

n>1时,上式为L=nR−SL仍然表示从链表头到达环入口的距离,而nR−S可以看成从cross出发移动nR步后再倒退S步,而明显从cross移动nR步后回到cross位置,倒退S步后是环入口,所以也是同时到达环入口。即二者相等时便是环入口节点

所以寻找环入口的方法就是采用两个指针,一个从表头出发,一个从相遇点出发,一次都只移动一步,当二者相等时便是环入口的位置

ListNode *findEnterNode(ListNode *head) {
	ListNode * slow = head;
	ListNode * fast = head;
	while(fast != null && fast->next != null)
	{
		slow = slow->next;
		fast = fast->next->next;
		//两指针相遇则有环
		if(slow == fast)
			break;
	}
	
	if(!fast || !fast->next)
		return nullptr;
	ListNode * headWalker = head;  // 从head开始走
	ListNode * crossWalker = slow; // 从相遇点开始走
	//两指针相遇的点即是入口节点, 前面已经证明了链表有环,并且不为空
	while(headWalker != crossWalker)
	{
		headWalker = headWalker->next;
		crossWalker = crossWalker->next;
	}
	return headWalker;
}

https://www.jianshu.com/p/7608f44e1baf

【图解LeetCode】142:环形链表,你的入口在哪? - 知乎

3.2 、计算环的长度

第一种方法是利用上面求出的环入口,再走一圈, 当node 和入口点相同时则走过了一圈,就可以求出长度,代码如下


int getlen(ListNode head) {

    ListNode cycleIn = getCycleEnter(head); //得到环的入口
    ListNode walker = cycleIn;
    int len = 1;
    while(walker->next != cycleIn)
    {
        ++len;
        walker = walker->next;
    }
    return len ; //直到找到相同指向的node,这个就是入口
}

第二种方法是当快慢指针相遇时,继续移动直到第二次相遇,此时快指针移动的距离正好比慢指针多一圈

int cycleLen(ListNode* head)
{
    ListNode× slow = head; 
    ListNode× fast = head; 
    while(fast != null && fast->next != null )
    {
        slow = slow->next;
        fast = fast->next->next;
        if(slow == fast)
            break;
    }
    int len = 0;
    // 找到相遇点 slow == fast,再走一圈相遇即为环长度
    while(fast!= null && fast->next!= null)
    {
        ++len;
        slow = slow->next;
        fast = fast->next->next;
        if(slow == fast)
            break;
    }
    return len;
}

具体参考:每天一道LeetCode-----判断链表是否有环,如果有,找到环的入口位置_一个程序渣渣的小后院的博客-CSDN博客_链表判断是否有环

4、判断链表是否相交,并且求出第一个交点

思路:1、如果两个链表相交,那么它们一定有相同的尾结点,遍历两个链表,找出尾结点,如果尾结点相同,那么这两个链表相交,反之不相交

2.使用栈。
我们可以从头遍历两个链表。创建两个栈,第一个栈存储第一个链表的节点,第二个栈存储第二个链表的节点。每遍历到一个节点时,就将该节点入栈。两个链表都入栈结束后。则通过top判断栈顶的节点是否相等即可判断两个单链表是否相交。因为我们知道,若两个链表相交,则从第一个相交节点开始,后面的节点都相交。
若两链表相交,则循环出栈,直到遇到两个出栈的节点不相同,则这个节点的后一个节点就是第一个相交的节点。

void findnode(Node* head1,  Node* head2) {

Stack<Node> stack1=new Stack<>();
Stack<Node> stack2=new Stack<>();

while(head1 != NULL) {
    stack1.push(head1);
    head1 = head1->next;
}
while(head2 != NULL) {
    stack2.push(head2);
    head2 = head2->next;
}
node temp=NULL;  //存第一个相交节点
while(!stack1.empty()&&!stack1.empty())  //两栈不为空
{
    temp=stack1.top();  
    stack1.pop();
    stack2.pop();
    if(stack1.top()!=stack2.top())
    {
        break;
    }
}

 return temp
}


这个方法在没有要求空间复杂度的时候,使用栈来解决这个问题也是挺简便的。
 

3、java中HashSet<> 可以判断是否包含元素。可以用两个HashSet,set1 和 set2 来存放链表的node,每一次新加node之前判断在另一个set中是否已经包含了,如果有则证明链表相交,同时这个就是交点

void CrossLink(Li)

boolean CrossLink(ListNode head1, ListNode head2) {
    Set<ListNode> set1 = new HashSet<>();
    Set<ListNode> set2 = new HashSet<>();

   while (head1 != null  &&  head2 != null) {

        if(set1.contain(head2))
           return true;

        if(set2.contain(head1))
           return true;
        
        head1 = head1->next
        head2 = head2->next
      
    }
    return false;
}

5、两个有序链表合并

leetcodeb 21:力扣

ListNode* mergeTwoLists(ListNode* l1, ListNode* l2)
 {
     // l1、l2至少一个为空
    if(!l1 || !l2)
    {
        // (l1 && !l2)为true表示l2为空,则返回l1,否则返回l2
        // 返回l2的情况:(1)l1为空,l2不空;(2)l1、l2都为空。
        return l1 && !l2 ? l1 : l2;
    }
	
    //声明一个头结点,赋初值为-1,这个值没啥用
	ListNode* preHead = new ListNode(-1); 

    //记录需要插入的上一个节点,方便插入
	ListNode* prev = preHead;

	while (l1 != nullptr && l2 != nullptr) {
		if (l1->val < l2->val)
		{   
			prev->next = l1;
			l1 = l1->next;
		} else {
			prev->next = l2;
			l2 = l2->next;
		}
		
		// 经过if之后,当前节点指向了prev->next,
		//将 prev->next 赋值为下一个需要被插入的前几点
		prev = prev->next;
	}

	// 合并后 l1 和 l2 最多只有一个还未被合并完,我们直接将链表末尾指向未合并完的链表即可
	prev->next = l1 == nullptr ? l2 : l1;
    
    //  preHead->next 指向的才是第一个节点的位置
	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;
        }

    }
}

6、链表去重

力扣

有序链表:

struct ListNode* deleteDuplicates(struct ListNode* head) {
    if (!head) {
        return head;
    }

    struct ListNode* cur = head;
    while (cur->next) {
        if (cur->val == cur->next->val) {
            cur->next = cur->next->next;
        } else {
            cur = cur->next;
        }
    }
    return head;
}

无序链表:

//删除方法二:进行双重循环遍历,外循环当前遍历的结点为cur,内循环从cur开始遍历,相同则删除 

//时间复杂度O(N2),空间复杂度O(1)
public void removeDuplicatedElements_1(Node head) {
    Node pcur = head;  //从第一个节点开始向后遍历
    Node pre = NULL;   //前一节点
    Node pnext = null;  //后面的节点,每次判断的是pcur 和next的节点值是否相同
    while (pcur != NULL) {
       
        pre = pcur;
        pnext = pcur.next;

        //pnext 指针遍历链表
        while (pnext != NULL) {
           //如果节点值相同,需要删除 pnext 节点,则上一节点pre.next 的指向变为 pnext.next 的地址,删除pnext节点
            if (pcur.data == pnext.data) {
                pre.next = pnext.next;
               // free(pnext); //释放内存
            } else {
                pre = pnext;
            }
            pnext = pnext.next; 
        }
        pcur = pcur.next;
    }
}

// 使用set
//时间复杂度O(N),空间复杂度O(N)
public void removeDuplicatedElements_2(Node head) {
    if (NULL == head)
        return;
    HashSet<Integer> hash_set = new HashSet<Integer>();
    Node pre = head;
    Node cur = head.next;
    hash_set.add(head.data);
    while (NULL != cur) {
        if (hash_set.contains(cur.data)) {
            pre.next = cur.next;
        } else {
            hash_set.add(cur.data);
            pre = cur;
        }
        cur = cur.next;
    }
}

7 、链表中倒数第 K 个节点

【剑指offer】面试题22

注意,走了多少步就是过了多少距离,距离和长度不一样,比如以下这个例子的链表长度为n,但是从头节点走到n节点要走n-1步,也就是头节点到尾节点的距离是n-1。

思路1:假设整个链表有N个结点,那么倒数第k个结点就是从头结点开始的第n-k-1个结点。如果我们只要从头结点开始往后走n-k+1步就可以了。如何得到节点数n?这个不难,只需要从头开始遍历链表,每经过一个结点,计数器加1就行了。这样需要遍历两边链表

思路2:定义两个指针。第一个指针从链表的头指针开始遍历向前走k-1。第二个指针保持不动;从第k步开始,第二个指针也开始从链表的头指针开始遍历。由于两个指针的距离保持在k-1,当第一个(走在前面的)指针到达链表的尾指结点时,第二个指针正好是倒数第k个结点。

鲁棒性判断:

1、输入Head指针为null。由于代码会试图访问空指针指向的内存,程序会崩溃。

2、输入以Head为头结点的链表的结点总数少于k。由于在for循环中会在链表向前走k-1步,仍然会由于空指针造成崩溃。

3、输入的参数k为0或负数,同样会造成程序的崩溃。

public ListNode getKthFromEnd(ListNode head, int k){
	// 鲁棒性的体现1:输入参数的检查
	if(head == null || k <= 0){
		return null;
	}
	// 定义两个指针
	ListNode slow = head;
	ListNode fast = head;
	
	// 先走 k-1 步
	for (int i = 0; i < k - 1; i++) {
		if(fast.next != null){
			// 遍历链表,直到找到尾节点,这样就可以直到链表中总的节点个数了
			fast = fast.next;
		}else{
			// 鲁棒性的体现2:链表中总的节点数小于k,直接返回null
			return null;
		}
	}
	// 此时fast指针走了k-1步,后面两个指针一起向后遍历
	// 当fast到达尾节点的时候,slow正好到达倒数第k个节点
	while(fast.next != null){
		slow = slow.next;
		fast = fast.next;
	}
	return slow;
}

更简洁一点的写法:

public ListNode getKthFromEnd(ListNode head, int k){
	
	if(head == null || k <= 0){
		return null;
	}
	// 定义两个指针
	ListNode slow = head;
	ListNode fast = head;

	for (int i = 0; fast != null; i++) {
		if(i >= k){
			// 此时fast指针走了k-1步,后面两个指针一起向后遍历
			// slow 指针从head
			slow = slow.next;
		}
		fast = fast.next;
	}
	// 考虑鲁棒性
	//	return i < k ? null : slow;
	if( i < k) {
	    return null;
	} else {
		return slow;
	}
}

8、链表快速排序

单链表的快速排序

首先,很容易想到的是:
1. 要做一轮基准值定位,怎么做?
2. 要做左子链表和右子链表的递归,怎么做?

第二个问题比较好回答,只要知道子链表的首尾节点,就可以做递归了。伪代码是:

void quick_sort_link(Node *start, Node *end=NULL);

第一个问题才是要解决的难题。思路如下:
     假设第一轮基准值定位做完了,我们需要有什么才能继续进行?
很显然,需要有左子链表和右子链表的各自的首尾节点。那么,左链表的首节点和右链表的尾节点,这2个一开始就有了。所以,需要有的是:左子链表的尾节点,和 右子链表的首节点。而这2个节点分别位于基准值节点的左边和右边。

这个时候,有一个思路是:使用2个辅助指针 p1 和 p2.

p1 负责维护左子链表,它是左子链表的最后一个节点;
p2 负责维护右子链表,它不断右移:其实,相当于p2在不断扩充右子链表,而待探索区不断缩小
当p2在探索区发现大值的时候,只需右移即可,将其纳入右子链表的范围;
当p2发现小值的时候,就要把p1右移一个(相当于扩大左子链表的范围),然后交换p1和p2的值(把小值和原来右子链表的最后一个节点交换),然后p2继续右移。

到最后,循环结束的时候,还需要交换基准值(pstart)和p1的值,因为,基准值从来没有动过,还在第一个节点的位置,而p1最终已经指向左子链表的最后一个位置,因此需要交换它们2个。

/*
    基准值是start->data;
    将原链表看作2个链表:左链表和右链表,左链表最后一个节点就是基准值
    p1是左链表的最后一个节点,p2是右链表的最后一个节点
    因此,当遇到大于基准值的时候,p2一直右移;
    当遇到小于基准值的时候,p1右移一个,再交换p1和p2的值,相当于维持了p1和p2的定义
    一轮循环的最后,p2到达了end的位置,此时,应该交换p1和start节点的值,这时才是真正的一轮处理的结束
    下一轮,就递归调用 qs(start, p1) 和 qs(p1->next, end) 了。
*/
void quick_sort_list(Node* start, Node* end=NULL)
{
    if (start == NULL || start == end) return;

    int key = start->data
    Node* p1 = start;
    Node* p2 = start->next; 

    while (p2 != end) {
        if (p2->data < key ) {
            p1 = p1->next;
            swap(p1->data, p2->data);
        }
        p2 = p2->next;
    }
    swap(p1->data, start->data);

    quick_sort_list(start, p1);
    quick_sort_list(p1->next, end);
}

//使用快慢指针,得到分组后基准点的pnode
Node* GetPartion(Node* pstart, Node* pEnd)
{
    int key = pBegin->key; //做基准的数值
    Node* slow = pstart; 
    Node* fast = slow->next;

    while(fast != pEnd)
    {
        if(fast->key < key)
        {
            slow = slow->next;
            swap(slow->key, fast->key);
        }
        fast = fast->next;
    }
    swap(slow->key, pstart->key);  //基准元素归为
    return slow;
}

void QuickSort(Node* pstart, Node* pEnd)
{
    if(pBeign != pEnd)
    {
        Node* pNode = GetPartion(pstart,pEnd); 
        QuickSort(pstart, pNode);
        QuickSort(partion->next, pNode);
    }
}

参考:单链表的快速排序与归并排序_执假以为真的博客-CSDN博客_单链表的快速排序

LeetCode:归并排序:力扣

排序链表「八大排序算法」 力扣

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值