面试之链表


前言


这一篇博客是很早之前写的,是关于一些链表和二叉树面试相关的问题,算是整理吧,网上这部分的答案也很多,希望能给大家一些帮助。

注意:本文中一些异常情况都是没有做处理的,例如NULL等等,只是给出了基本的解决方案.大家参考一下.


链表部分



问题:定义并且创建一个链表.

解题方案:
我们首先要如何定义一个结构体,下面的构造方案我是使用的递归的形式来构造一个结构体,注意不要忘记分配内存.其他的方面都比较简单,难度较低.

代码示例:

#include <stdio.h>

typedef struct ListNode {
    int data;
    struct ListNode*nextNode;
}ListNode;

ListNode* createListNodeAction(int *listArray, int index, int length) {
    ListNode *listNode = (ListNode *) malloc(sizeof (ListNode) );
    ListNode *nextNode = NULL;
    int i = listArray[index];
    listNode->data = i;
    if (index != length - 1) {
        nextNode = (ListNode*) malloc(sizeof (ListNode));
        nextNode = createListNodeAction(listArray, index + 1, length);
    }
    listNode->nextNode = nextNode;
    return listNode;
}



问题:不通过遍历删除链表中的非尾节点.

解题方案:
首先我们要知道我们如何通过遍历删除链表中的某个节点? 通过遍历我们可以知道要删除的链表节点前驱(也就是前一个节点),然后我们把前驱的nextNode指向要删除的节点的nextNode,释放要删除的节点即可.示意图如下所示.

那么我们对于上面的那个题目,我们该如何解决呢?由于前驱不通过遍历我们是拿不到的,所以我们只能通过覆盖的形式,用nextNode节点的属性覆盖掉需要删除的节点,然后释放nextNode节点,这样就完成了删除工作,由于前驱的nextNode指针属性不通过遍历修改不了,所以不能删除尾节点.否则就会有野指针问题出现.

代码示例:

void deleteListNodeNotTail(ListNode *deleteNode) {
  
    ListNode *deleteNextNode = deleteNode->nextNode;
    deleteNode->data = deleteNextNode->data;
    deleteNode->nextNode = deleteNextNode->nextNode;
    free(deleteNextNode);
}



问题:只遍历一次就找到链表中的中间节点.

解题方案:
撇开题目不谈,我们首先要清楚如何确定链表中的中间节点?由于链表没有长度的属性,所以暴力法的做法就是先遍历一次确定链表的长度,然后再次遍历链表找到中间节点.时间复杂度为O(logn+n).

那么如何通过一次遍历来找到链表中的中间节点呢?我们的解决方案是我们需要一快一慢两个移动节点fathNodeslowNode,fathNode的偏移速度是slowNode的两倍,,所以当fathNode == NULL,slowNode正好处于中心节点上.时间复杂度为O(logn).

代码示例:

ListNode* getListHalfNode(ListNode *listNode) {
    
    ListNode *fathNode = listNode->nextNode;
    ListNode *slowNode = listNode;

    while (fathNode) {
        fathNode = fathNode->nextNode->nextNode;
        slowNode = slowNode->nextNode;
    }
    return slowNode;
}



问题:如何找到单向链表中的倒数第i个节点(i >= 1).

解题方案:
暴力法该如何解决这种问题呢?我们先遍历一遍确定链表的长度length,再次遍历链表取得下标位置在length-1-k的节点就是我们要的节点.时间复杂度为O(logn+n).

那没有有没有优化方式呢?这是有的,仍然借助上一个问题的解决方案,我们需要一快一慢两个移动节点fathNodeslowNode,fathNode先偏移i个位置,然后两个节点同时进行移动,所以当fathNode == NULL,slowNode正好处于倒数.时间复杂度为O(2logn).

代码示例:

ListNode* getListNodeWithLast(ListNode *listNode,int i) {
    
    ListNode *fathNode = listNode;
    ListNode *slowNode = listNode;
    
    while (i) {
        fathNode = fathNode->nextNode;
        i--;
    }
    
    while (fathNode) {
        fathNode = fathNode->nextNode;
        slowNode = slowNode->nextNode;
    }
    return slowNode;
}



问题:删除倒数第i个结点(i>=1),不能用替换删除法.

解题方案:
上面我们已经了解了替换删除法,不需要知道前驱,我们就可以使用覆盖替换的方式删除节点,而这次我们可以是知道前驱节点的,而且结合上一次的快慢节点的方式,我们只需要先找到前驱节点即可.也就是fathNode节点需要先移动i + 1 次,具体代码如下所示.

代码示例:

void deleteListNodeWithLast(ListNode *listNode,int i) {
    
    ListNode *fathNode = listNode;
    ListNode *slowNode = listNode;
    
    while (i + 1) {
        fathNode = fathNode->nextNode;
        i--;
    }
    
    while (fathNode) {
        fathNode = fathNode->nextNode;
        slowNode = slowNode->nextNode;
    }
    
    ListNode *deleteNode = slowNode->nextNode;
    ListNode *deleteNextNode = deleteNode->nextNode;
    slowNode->nextNode = deleteNextNode;
    free(deleteNode);
}



问题:约瑟夫问题

约瑟夫环(约瑟夫问题)是一个数学的应用问题:已知n个人(以编号1,2,3…n分别表示)围坐在一张圆桌周围。从编号为k的人开始报数,数到m的那个人出列;他的下一个人又从1开始报数,数到m的那个人又出列;依此规律重复下去,直到圆桌周围的人全部出列。通常解决这类问题时我们把编号从0~n-1,最后结果+1即为原问题的解。

解题方案:
使用链表该如何解决约瑟夫问题呢?我们需要把链表做成一个环,也就是我们需要遍历一遍找到尾节点,并且制定尾节点的nextNode指针指向链表的第一个节点,这样我们就把链表做成了一个环.

然后我们假设每i次删除一个节点,这样返回的删除,直到只剩最后一个节点就是我们要求的解.

代码示例:

ListNode* JocephCircle(ListNode *firstNode, int k) {
    
    ListNode *endNode = firstNode;
    ListNode *resultNode = firstNode;
    ListNode *deleteNode = NULL;
    
    // 做环
    while (endNode->nextNode) {
        endNode = endNode->nextNode;
    }
    endNode->nextNode = firstNode;

    // 自身的nextNode指向自身的时候,就只剩下一个元素了
    while (resultNode->nextNode != resultNode) {
        
        //删除节点 ,先找到前驱节点,然后找到删除节点
        //由于先执行赋值操作,再进行i-1操作,所以k-1,由于是找删除节点的前驱节点,所以还需要-1.
        int i = (k-1)-1;
        while (i) {
            resultNode = resultNode->nextNode;
            i--;
        }
        
        // 重新指向并且释放删除节点
        deleteNode = resultNode->nextNode;
        resultNode->nextNode = resultNode->nextNode->nextNode;
        free(deleteNode);
        resultNode = resultNode->nextNode;
    }
    
    return resultNode;
}



问题:单链表的冒泡排序问题

解题方案:
仿照普通的数组遍历,这里两个while进行实现简单的冒泡排序.判断条件为nextNode节点是否为NULL,即可知道是否已经到达了单链表的尾节点.这个问题如果不做任何优化的话就如同下面代码演示的即可.其他优化方式就不过多阐述,上网查询即可.

代码示例:

void sortNodeListAction(ListNode *firstNode) {
 
    ListNode *nowNode = firstNode;
    ListNode *exchangeNode = (ListNode *)malloc(sizeof(ListNode));
    
    while (nowNode->nextNode) {
        ListNode *nowNextNode = nowNode;
        while (nowNextNode) {
            if (nowNextNode->data < nowNode->data) {
                exchangeNode->data = nowNextNode->data;
                nowNextNode->data = nowNode->data;
                nowNode->data = exchangeNode->data;
            }
            nowNextNode = nowNextNode->nextNode;
            if (!nowNextNode) {
                continue;
            }
        }
        nowNode = nowNode->nextNode;
    }
    free(exchangeNode);
}



问题:判断链表是否带环;若带环,求环的长度和入口点

解题方案:
这里我们要首先明白什么叫做带环,如下图所示,不管是哪种表现形式,我们都说当前链表是带环的链表.

我们了解了什么叫链表带环.在代码中,我们该如何判断当前的链表是否带环呢?网上有一种方案就是使用快慢节点解决,设置fathNodeslowNode,fathNode的偏移速度是slowNode的两倍,所以当fathNode == NULL,那么可以断定链表不带环,假设在某一个时刻fathNode==slowNode,说明两个节点重合,也就是说链表带环.

那么带环的链表我们该如何判断其环的长度呢?首先我们要知道fathNode偏移速度是slowNode的两倍,也就是说相同时间内,fathNode偏移距离是slowNode2倍.

我们要说明两个节点交汇的情况,两者的情况肯定是慢节点在换上走不到一圈就会进行交汇,有人会问这是为什么呢?因为fathNode偏移速度是slowNode的两倍,所以在两者起点相同的情况下slowNode走完一圈fathNode走完两圈内,两者是必然相交的.

根据上面的两种情形,如下图所示.当两点相交时,我们有以下的结论,fathNode走过的路程为L + (C + A) + A,slowNode走过的路程为L + A, 我们得出 (L + A) x 2 = L + (C + A) + A;所以L = C.这时候我们继续定义一个新的节点enterNode从头开始出发,slowNode同时出发,两者速度相同,同时L = C;所以我们知道两者相交的节点必然是环的入口点.这时候enterNode再走到b点,就可以计算出环的长度了.

代码示例:

// 判断是否有环
bool isExistLoop(ListNode* firstNode)  {  
    ListNode *fastNode;
    ListNode * slowNode;
    fastNode = slowNode = firstNode;
    while (slowNode != NULL && fastNode -> next != NULL)  {  
        slowNode = slowNode -> next ;
        fastNode = fastNode -> next -> next ; 
        if (slowNode == fastNode) 
            return true ;  
    }  
    return false ;  
} 
// 判断环的长度
int getLoopLength(ListNode* firstNode){
    ListNode* slowNode = firstNode;
    ListNode* fastNode = firstNode;
    while ( fastNode && fastNode ->next ){
        slowNode = slowNode->next;
        fastNode = fastNode->next->next;
        if ( slowNode== fastNode) {
            break;
         }
    }
    slowNode= slowNode->next;
    fastNode = fastNode->next->next;
    int length = 1; 
    while ( fastNode != slowNode)
    {
        slowNode = slowNode->next;
        fastNode = fastNode->next->next;
        length ++; 
    }
    return length;
}
// 找到环中的相遇节点
ListNode* getMeetingNode(ListNode* firstNode) {
    ListNode* fastNode;
    ListNode* slowNode;
    slowNode = fastNode = firstNode;
    while (slowNode != NULL && fastNode-> next != NULL) {  
        slowNode = slowNode-> next ;
        fastNode = fastNode-> next -> next ;
        if (slowNode == fastNode)
            return slowNode;  
    }  

    //到达末尾仍然没有相遇,则不存在环
    return NULL ;
}
// 找出环的入口节点
ListNode* getEntryNodeOfLoop(ListNode* firstNode) {
    ListNode* meetingNode = getMeetingNode(firstNode); // 先找出环中的相遇节点
    if (meetingNode == NULL)
        return NULL;
    ListNode* p1 = meetingNode;
    ListNode* p2 = pHead;
    while (p1 != p2) {
        p1 = p1->next;
        p2 = p2->next;
    }
    return p1;
}

如果可以使用字典或者集合的话,那就更简单了;数组也是可以解决,但是效率不是太高.需要多次遍历.



总结


OK,写到这里基本上就结束了,先整理这些后期会持续更新,欢迎大家指导批评,谢谢。。。


最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270

推荐阅读更多精彩内容

  • 1. 链表 链表是最基本的数据结构,面试官也常常用链表来考察面试者的基本能力,而且链表相关的操作相对而言比较简单,...
    Mr希灵阅读 5,544评论 1 17
  • 摘自《维基百科》 链表(Linked list)是一种常见的基础数据结构,是一种线性表,但是并不会按线性的顺序存储...
    ChinaChong阅读 1,645评论 0 52
  • 代码GitHub地址 链表概述 数组和链表都是线性存储结构的基础实现,栈和队列都是线性存储结构的应用 数组优缺点 ...
    HikariCP阅读 1,293评论 0 0
  •  接续上一篇《数据结构系列:Objective-C实现单链表》  双向链表 摘自《双向链表-维基百科,自由的百科全...
    ChinaChong阅读 1,240评论 0 52
  • 数据结构与算法 一 简介 单链表中的每个结点不仅包含值,还包含链接到下一个结点的引用字段。image 1.1 结点...
    凯玲之恋阅读 27,608评论 1 15