数据结构 - 链表 - 面试中常见的链表算法题

数据结构是面试中必定考查的知识点,面试者需要掌握几种经典的数据结构:线性表(数组、链表)、栈与队列(二叉树、二叉查找树、平衡二叉树、红黑树)、

本文主要介绍线性表中的常见的链表数据结构。包括

  • 概念简介
  • 链表节点的数据结构(Java)
  • 常见的链表算法题(Java)。

概念简介

如果对链表概念已经基本掌握,可以跳过该部分,直接查看常见链表算法题。

线性表基本概念

链表是一种线性表,因此我们首先了解一下什么是线性表:

线性表是最常用且最简单的一种数据结构,它是n个数据元素的有限序列。实现线性表的方式一般有两种:

  • 使用数组存储线性表的元素,即用一组连续的存储单元依次存储线性表的数据元素。
  • 使用链表存储线性表的元素,即用一组任意的存储单元存储线性表的数据元素(存储单元可以是连续的,也可以是不连续的)。

链表基本概念

链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。

链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域

相比于线性表顺序结构(数组),操作复杂。由于不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而线性表和顺序表相应的时间复杂度分别是O(logn)和O(1)。

使用链表结构可以克服数组链表需要预先知道数据大小的缺点,链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但是链表失去了数组随机读取的优点,同时链表由于增加了结点的指针域空间开销比较大
链表最明显的好处就是,常规数组排列关联项目的方式可能不同于这些数据项目在记忆体或磁盘上顺序,数据的存取往往要在不同的排列顺序中转换。链表允许插入和移除表上任意位置上的节点,但是不允许随机存取。

链表有很多种不同的类型:单向链表双向链表以及循环链表

数组和链表的区别

  • 数组是同类型的连续的一个存储空间,链表是不连续的,其元素结点是一个结构体。
  • 数组是在栈中分配的,即数组大小在编译时就已经确定,即内存是静态分配的;链表是在堆中分配的,运行过程才具体分配,即链表是动态分配内存。
  • 数组对于元素的查询是通过下标直接索引,而链表是通过结点之间的链接一步一步进行遍历。数组对元素的查询时间复杂度是O(1),链表对元素的查询时间复杂度是O(n).
  • 数组对于元素的插入、删除效率较低,需要进行挪位。而链表插入,删除只需要操作相应位置上的指针即可。数组对元素的插入、删除时间复杂度是O(n),链表对元素的插入、删除时间复杂度是O(1)。

链表节点的数据结构

链表由一系列结点组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。

定义节点为类:ListNode。具体实现如下:

public class ListNode {public int val; // 数据域public ListNode next; // 指针域public ListNode() {}public ListNode(int val) {this.val = val;}// 打印链表节点元素值public static void printList(ListNode head) {while (head != null) {System.out.print(head.val + "->");head = head.next;}System.out.println("null");}}

常见的链表算法题(单链表)

1. 求单链表中结点的个数
/*** 1. 求单链表中节点的个数:* 注意检查链表是否为空。时间复杂度为O(n)。这个比较简单。* @param head 链表头结点* @return 链表中节点的个数*/
public static int getLength(ListNode head) {if (head == null) {return 0;}int length = 0;ListNode current = head;while (current != null) { // 当前元素不为空length++;current = current.next;}return length;
}
2. 查找单链表中的倒数第k个结点(剑指offer,题15)

思路:这里需要声明两个指针:即两个结点型的变量first和second,首先让first和second都指向第一个结点,然后让second结点往后挪k-1个位置,此时first和second就间隔了k-1个位置,然后整体向后移动这两个节点,直到second节点走到最后一个结点的时候,此时first节点所指向的位置就是倒数第k个节点的位置。时间复杂度为O(n)。

/*** 2. 查找单链表中的倒数第k个结点(剑指offer,题15)* 时间复杂度为O(n)** 考虑k=0和k大于链表长度的情况* @param head 链表头结点* @param k 倒数第k* @return 倒数第k个节点*/
public static ListNode findLastNode(ListNode head, int k){if (head == null || k <= 0) { // 输入异常throw new RuntimeException("输入参数格式不对...");}ListNode first = head; // 两个指针ListNode second = head;for (int i = 0; i < k - 1; i++) {second = second.next;if (second == null) { // 说明k的值大于链表的长度throw new RuntimeException("k越界");}}// 两个指针同时移动,second到达尾节点时,first是倒数第k个节点while (second.next != null) {first = first.next;second = second.next;}return first;
}
3. 查找单链表中的中间结点

面试官不允许你算出链表的长度,该怎么做呢?
思路:和上面的第2节一样,也是设置两个指针first和second,只不过这里是,两个指针同时向前走,second指针每次走两步,first指针每次走一步,直到second指针走到最后一个结点时,此时first指针所指的结点就是中间结点。

注意链表为空,链表结点个数为1和2的情况。时间复杂度为O(n)。

/*** 3. 查找单链表中的中间结点:* 上方代码中,当n为偶数时,得到的中间结点是第n/2个结点。比如链表有6个节点时,得到的是第3个节点。* @param head 链表头结点* @return 中间节点*/
public static ListNode findMidNode(ListNode head){if (head == null || head.next == null || head.next.next == null) {return null;}ListNode first = head;ListNode second = head;while (second.next != null && second.next.next != null) {first = first.next;second = second.next.next;}return first;
}
4. 合并两个有序的单链表,合并之后的链表依然有序【出现频率高】(剑指offer,题17)

这道题经常被各公司考察。例如:

链表1:1->2->3->4;
链表2:2->3->4->5;
合并后:1->2->2->3->3->4->4->5

解题思路:挨个比较链表1和链表2。这个类似于归并排序。
尤其要注意两个链表都为空、和其中一个为空的情况。
只需要O(1)的空间。时间复杂度为O (max(len1,len2))。

/*** 4. 合并两个有序的单链表,合并之后的链表依然有序【出现频率高】(剑指offer,题17)*  解题思路:挨着比较链表1和链表2。这个类似于归并排序。尤其要注意两个链表都为空、和其中一个为空的情况。*  只需要O(1)的空间。时间复杂度为O (max(len1,len2))* @param head1 链表1头结点* @param head2 链表2头结点* @return 合并后的链表头结点*/
public static ListNode mergeLinkList(ListNode head1, ListNode head2) {// 第一个链表为空if (head1 == null) {return head2;}// 第二个链表为空if (head2 == null) {return head1;}// 设置链表头结点ListNode head = new ListNode(-1);ListNode temp = head;while (head1 != null && head2 != null) {if (head1.val < head2.val) { // 链表1的元素小于链表2的元素temp.next = head1;head1 = head1.next;} else {temp.next = head2;head2 = head2.next;}temp = temp.next;}// 链表1没有遍历结束if (head1 != null) {temp.next = head1;}// 链表2没有遍历结束if (head2 != null) {temp.next = head2;}return head.next; // 返回空节点的下一个节点
}
5. 单链表的反转:【出现频率最高】(不使用额外的空间)

例如:

链表:1->2->3->4
反转之后:4->3->2->1

思路:从头到尾遍历原链表,每遍历一个结点,将其摘下放在新链表的最前端。
注意链表为空和只有一个结点的情况。时间复杂度为O(n)。

/*** 5. 单链表的反转:【出现频率最高】(不使用额外的空间)* 例如链表:1->2->3->4     反转之后:4->3->2->1* 思路:从头到尾遍历原链表,每遍历一个结点,将其摘下放在新链表的最前端。* 注意链表为空和只有一个结点的情况。时间复杂度为O(n)* @param head 链表头结点* @return 链表反转后的头结点*/
public static ListNode reverseList(ListNode head) {if (head == null || head.next == null) {return head;}ListNode newHead = null; // 保存链表新表头ListNode current = head; // 保存当前链表的遍历节点while (current != null) {ListNode next = current.next; // 保存当前节点的下一个节点current.next = newHead;newHead = current;current = next;}return newHead;
}
6. 从尾到头打印单链表

递归实现:用递归实现,但有个问题:当链表很长的时候,就会导致方法调用的层级很深,有可能造成栈溢出。

/**
* 6.从尾到头打印单链表
* 用递归实现,但有个问题:当链表很长的时候,就会导致方法调用的层级很深,有可能造成栈溢出。
* 注意链表为空的情况。时间复杂度为O(n)
* @param head 链表头结点
*/
public static void reversePrint(ListNode head) {if (head == null) {return;}reversePrint(head.next);System.out.print(head.val + "->");
}

非递归实现:对于这种颠倒顺序的问题,我们应该就会想到栈,后进先出。显式用栈,是基于循环实现的,代码的鲁棒性要更好一些。
注意链表为空的情况。时间复杂度为O(n)。

/*** 6. 从尾到头打印单链表* 对于这种颠倒顺序的问题,我们应该就会想到栈,后进先出。* 显式用栈,是基于循环实现的,代码的鲁棒性要更好一些。* 注意链表为空的情况。时间复杂度为O(n)* @param head 链表头结点*/
public static void reversePrintByStack(ListNode head) {if (head == null) {return;}Stack<ListNode> stack = new Stack<>();while (head != null) { // 将链表元素压入栈中stack.add(head);head = head.next;}while (!stack.isEmpty()) { // 将链表元素出栈打印System.out.print(stack.pop().val + "->");}
}
7. 判断单链表是否有环

这里也是用到两个指针,如果一个链表有环,那么用一个指针去遍历,是永远走不到头的。因此,我们用两个指针去遍历:first指针每次走一步,second指针每次走两步,如果first指针和second指针相遇,说明有环。
时间复杂度为O (n)。

/*** 7. 判断单链表是否有环:* 这里也是用到两个指针,如果一个链表有环,那么用一个指针去遍历,是永远走不到头的。* 因此,我们用两个指针去遍历:first指针每次走一步,second指针每次走两步,* 如果first指针和second指针相遇,说明有环。时间复杂度为O (n)。* @param head 链表头结点* @return 是否存在环*/
public static boolean hasCycle(ListNode head) {if (head == null || head.next == null) {return false;}ListNode first = head; // 每次移动一步ListNode second = head; // 每次移动两步while (second != null && second.next != null) { // 判断空指针first = first.next;second = second.next.next;if (first == second) {return true;}}return false;
}
8. 取出有环链表中环的长度

思路:环的长度即为从开始到相遇处first走的步数。

 /*** 8. 取出有环链表中,环的长度:从开始到相遇处first走的步数* @param head 链表头结点* @return 环的长度*/
public static int getCycleLength(ListNode head){if (head == null || head.next == null) {return 0;}int length = 0; // 环的长度ListNode first = head; // 每次移动一步ListNode second = head; // 每次移动两步while (second != null && second.next != null) { // 判断空指针first = first.next;second = second.next.next;length++;if (first == second) {return length;}}return 0;
}
9. 有环单链表中,取出环的起始点

思路:从相遇点开始,设置一个节点从头开始,然后最终相遇的节点就是环的起始点。由上图中的a=c可知。

/*** 9、单链表中,取出环的起始点:从相遇点开始,设置一个节点从头开始,然后最终相遇的节点就是环的起始点。* @param head 链表头结点* @return 链表中环的起始节点*/
public static ListNode getCycleStart(ListNode head) {if (head == null || head.next == null) {return null;}ListNode first = head; // 每次移动一步ListNode second = head; // 每次移动两步while (second != null && second.next != null) { // 判断空指针first = first.next;second = second.next.next;if (first == second) {ListNode temp = head;while (temp != second) {temp = temp.next;second = second.next;}return second;}}return null;
}
10. 判断两个单链表相交的第一个交点。 剑指offer,题37。

思路:先遍历两个链表得到长度差,让长的链表先走长度差步,然后再同时走相遇的第一个节点就是返回结果。

/*** 10、判断两个单链表相交的第一个交点。 剑指offer,题37。* 先遍历两个链表得到长度差,让长的链表先走长度差步,然后再同时走相遇的第一个节点就是返回结果。* 时间复杂度为:O(m+n)* @param head1 链表1头结点* @param head2 链表2头结点* @return 相交的第一个节点*/
public static ListNode meetNode(ListNode head1, ListNode head2){if (head1 == null || head2 == null) {return null;}int len1 = 0;int len2 = 0;ListNode temp1 = head1;ListNode temp2 = head2;while (temp1 != null) {len1++;temp1 = temp1.next;}while (temp2 != null) {len2++;temp2 = temp2.next;}int diff = Math.abs(len1 - len2);ListNode longHead = head1;ListNode shortHead = head2;if (len1 < len2) {longHead = head2;shortHead = head1;}for (int i = 0; i < diff; i++) {longHead = longHead.next;}while (longHead != null && shortHead != null && longHead != shortHead) {longHead = longHead.next;shortHead = shortHead.next;}return longHead;
}
11. 以 k 个节点为段,反转单链表。

Reverse Nodes in k_Group,Leetcode上的算法题,第5题的高级变种

/*** 11、以 k 个节点为段,反转单链表。* Reverse Nodes in k_Group,Leetcode上的算法题,第6题的高级变种* @param head 链表头结点* @param k 每k个节点反转* @return 反转后的链表头*/
public static ListNode reverseKGroup2(ListNode head, int k) {ListNode curr = null;int count = 0;while (curr != null && count != k) { // find the k+1 nodecurr = curr.next;count++;}if (count == k) { // if k+1 node is foundcurr = reverseKGroup2(curr, k); // reverse list with k+1 node as head// head - head-pointer to direct part,// curr - head-pointer to reversed part;while (count-- > 0) { // reverse current k-groupListNode tmp = head.next; // tmp - next head in direct parthead.next = curr; // preappending "direct" head to the reversed listcurr = head; // move head of reversed part to a new nodehead = tmp; // move "direct" head to the next node in direct part}head = curr;}return head;
}

数据结构 - 链表 - 面试中常见的链表算法题相关推荐

  1. 数据结构 - 二叉树 - 面试中常见的二叉树算法题

    数据结构 - 二叉树 - 面试中常见的二叉树算法题 数据结构是面试中必定考查的知识点,面试者需要掌握几种经典的数据结构:线性表(数组.链表).栈与队列.树(二叉树.二叉查找树.平衡二叉树.红黑树).图 ...

  2. 校招面试中常见的算法题整理【长文】

    ⭐️我叫恒心,一名喜欢书写博客的研究生在读生. 原创不易~转载麻烦注明出处,并告知作者,谢谢!!! 这是一篇近期会不断更新的博客欧~~~ 有什么问题的小伙伴 欢迎留言提问欧. 文章目录 前言 一.链表 ...

  3. JavaScript 面试中常见算法问题详解

    JavaScript 面试中常见算法问题详解,翻译自 https://github.com/kennymkchan/interview-questions-in-javascript.下文提到的很多问 ...

  4. c++ 查找 list中最长的字符串_查找不重复字符的最长子字符串(编程面试中常见题-用8种编程语言来回答)...

    查找不重复字符的最长子字符串(编程面试中常见题-用8种编程语言来回答) 给定一个字符串str,找到不重复字符的最长子字符串. 比如我们有 "ABDEFGABEF", 最长的字符串是 ...

  5. java面试技术问题_11个JAVA面试中常见技术问题

    原标题:11个JAVA面试中常见技术问题 大家在平常面试java的过程中都会遇到哪些难题呢?还有一些即将去面试java的童鞋们,你们想知道技术面试中会涉及到哪些点吗?达妹为你整理Java面试中会被问到 ...

  6. linux运维培训后面试,Linux运维岗位面试中常见的面试问题汇总

    今天小编要跟大家分享的文章是关于Linux运维岗位面试中常见的面试问题汇总.正准备参加Linux运维面试的小伙伴们来和小编一起看一看吧,希望本篇文章能够对正在从事Linux运维工作的小伙伴们有所帮助. ...

  7. Java面试中常见的高并发解决方案

    Java面试中常见的高并发解决方案 一般来讲,提高系统应对高并发能力的解决方案可以从以下几个方面入手: (1)高性能服务器 (2)高性能数据库 (3)高效编程语言 (4)高性能web容器 提高数据库性 ...

  8. 程序员面试需要刷力扣算法题吗

    这里写目录标题 1. 程序员面试需要刷力扣算法题吗 1.1. 算法题的一些特征 1.2. 为什么要考查算法 1.3. 目前面试主要考查 3 类 1. 程序员面试需要刷力扣算法题吗 1.1. 算法题的一 ...

  9. 为什么字节跳动的前端面试需要那么难的算法题?

    首先我来辟个谣: 随便打开一个招聘网站,你会发现前端工程师的岗位需求依旧庞大,大厂人才奇缺,就业薪资起点高,无行业限制. (数据来源:职友集) 前端开发的行业大环境 行业升级,如果说以前只会HTML. ...

最新文章

  1. 卫星系统采用的轨道类型
  2. php中return返回数组,PHP中return返回数组的一点用法
  3. show部分书...
  4. 智慧交通day02-车流量检测实现11:yoloV3模型
  5. mxnet img2rec的使用,生成数据文件
  6. 【kafka】kafka 消费 带有 kerberos认证的服务器
  7. python复制文件夹不阻塞_11.python并发入门(part14阻塞I/O与非阻塞I/O,以及引入I/O多路复用)...
  8. 使用文件流的方式将 DataTable 导入到 Excel 中
  9. WIN10 JDK + JCreator
  10. python 微信公众号发文章_Python 微信公众号文章爬取
  11. 卫星地面站的星地链路研究
  12. QTreeWidget支持双击编辑Item节点的内容
  13. 粗糙集理论介绍(一)(rough set)
  14. [转载] COM 套间
  15. 新冠治愈之旅和未来的时光
  16. VSCode鼠标滚轮控制字体大小
  17. 美团点评2020校招算法工程师编程题--工作安排--动态规划
  18. 命名自喜剧团体,宅男程序员三个月写出的编程语言是如何改变世界的?
  19. Linux高速下载工具aria2介绍及使用说明
  20. Unity学习-脚本基础part01

热门文章

  1. UVa12166 Equilibrium Mobile修改天平(二叉树+dfs)
  2. 视觉注意力机制(中)
  3. Team Work(CF 932 E)[bzoj5093][Lydsy1711月赛]图的价值
  4. STL中的find_if函数
  5. cocos2d-x游戏开发(七)对象释放时机
  6. 详细解析WSAEventSelect模型
  7. 判断字符串是否为回文(C语言 顺序栈)
  8. 飞哥:程序员完全没时间提升自己该怎么办?
  9. [译]提案:在Go语言中增加对持久化内存的支持
  10. 毕业五年的音视频开发工程师过得怎么样了?