数据结构 - 链表 - 面试中常见的链表算法题
数据结构是面试中必定考查的知识点,面试者需要掌握几种经典的数据结构:线性表(数组、链表)、栈与队列、树(二叉树、二叉查找树、平衡二叉树、红黑树)、图。本文主要介绍线性表中的常见的链表数据结构。包括
- 概念简介
- 链表节点的数据结构(Java)
- 常见的链表算法题(Java)。
概念简介
线性表基本概念
链表是一种线性表,因此我们首先了解一下什么是线性表:
线性表是最常用且最简单的一种数据结构,它是n个数据元素的有限序列。实现线性表的方式一般有两种:
使用数组存储线性表的元素,即用一组连续的存储单元依次存储线性表的数据元素。
使用链表存储线性表的元素,即用一组任意的存储单元存储线性表的数据元素(存储单元可以是连续的,也可以是不连续的)。
链表基本概念
链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。
链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。
相比于线性表顺序结构(数组),操作复杂。由于不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而线性表和顺序表相应的时间复杂度分别是O(logn)和O(1)。
使用链表结构可以克服数组链表需要预先知道数据大小的缺点,链表结构可以充分利用计算机内存空间,实现灵活的内存动态管理。但是链表失去了数组随机读取的优点,同时链表由于增加了结点的指针域,空间开销比较大。
链表最明显的好处就是,常规数组排列关联项目的方式可能不同于这些数据项目在记忆体或磁盘上顺序,数据的存取往往要在不同的排列顺序中转换。链表允许插入和移除表上任意位置上的节点,但是不允许随机存取。
链表有很多种不同的类型:单向链表,双向链表以及循环链表。
数组和链表的区别
数组是同类型的连续的一个存储空间,链表是不连续的,其元素结点是一个结构体。
数组是在栈中分配的,即数组大小在编译时就已经确定,即内存是静态分配的;链表是在堆中分配的,运行过程才具体分配,即链表是动态分配内存。
数组对于元素的查询是通过下标直接索引,而链表是通过结点之间的链接一步一步进行遍历。数组对元素的查询时间复杂度是O(1),链表对元素的查询时间复杂度是O(n).
数组对于元素的插入、删除效率较低,需要进行挪位。而链表插入,删除只需要操作相应位置上的指针即可。数组对元素的插入、删除时间复杂度是O(n),链表对元素的插入、删除时间复杂度是O(1)。
常见的链表面试题
- 求单链表中结点的个数
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个节点
输入一个链表,输出该链表中倒数第k个节点。为了符合大多数人的习惯,本题从1开始计数,即链表的尾节点是倒数第1个节点。例如,一个链表有6个节点,从头节点开始,它们的值依次是1、2、3、4、5、6。这个链表的倒数第3个节点是值为4的节点。
示例:
给定一个链表: 1->2->3->4->5, 和 k = 2.
返回链表 4->5.
解题思路
这题目和前面重复了,最简单的思路就是计算链表长度再减去k,接着从前往后遍历减去后的次数即可,代码看看即可。
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode getKthFromEnd(ListNode head, int k) {
int length = 0;
ListNode current = head;
while(current!=null) {
length++;
current = current.next;
}
length = length-k;
for(int i = 0;i<length;i++) {
head = head.next;
}
return head;
}
}
3 删除链表的节点
给定单向链表的头指针和一个要删除的节点的值,定义一个函数删除该节点。
返回删除后的链表的头节点。
输入: head = [4,5,1,9], val = 5
输出: [4,1,9]
解释: 给定你链表中值为 5 的第二个节点,那么在调用了你的函数之后,该链表应变为 4 -> 1 -> 9.
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode deleteNode(ListNode head, int val) {
if(head.val==val){
return head.next;
}
ListNode cur=head;
while(cur!=null&&cur.next!=null){
if(cur.next.val==val){
cur.next=cur.next.next;
}else
cur=cur.next;
}
return head;
}
}
4 移除重复节点
题目:编写代码,移除未排序链表中的重复节点。保留最开始出现的节点
示例1:
输入:[1, 2, 3, 3, 2, 1]
输出:[1, 2, 3]
示例2:
输入:[1, 1, 1, 1, 2]
输出:[1, 2]
**
* Definition for singly-linked list
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode removeDuplicateNodes(ListNode head) {
ListNode p=head;
while(p!=null){
ListNode q=p;
while(q.next!=null){
if(q.next.val==p.val){
q.next=q.next.next;
}else
q=q.next;
}
p=p.next;
}
return head;
}
}
5 删除排序链表中的重复元素(注意有顺序与第四题不同)
题目:给定一个排序链表,删除所有重复的元素,使得每个元素只出现一次。
示例 1:
输入: 1->1->2
输出: 1->2
示例 2:
输入: 1->1->2->3->3
输出: 1->2->3
**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public ListNode deleteDuplicates(ListNode head) {
if(head==null){
return null;
}
ListNode current=head;
while(current!=null&¤t.next!=null){
if(current.val==current.next.val){
current.next=current.next.next;
}else{
current=current.next;
}
}
return head;
}
}
6 从尾到头打印链表
题目:输入一个链表的头节点,从尾到头反过来返回每个节点的值(用数组返回)。
示例 1:
输入:head = [1,3,2]
输出:[2,3,1]
方法一:栈
栈的特点是后进先出,即最后压入栈的元素最先弹出。考虑到栈的这一特点,使用栈将链表元素顺序倒置。从链表的头节点开始,依次将每个节点压入栈内,然后依次弹出栈内的元素并存储到数组中。
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public int[] reversePrint(ListNode head) {
Stack<ListNode> stack = new Stack<ListNode>();
ListNode temp = head;
while (temp != null) {
stack.push(temp);
temp = temp.next;
}
int size = stack.size();
int[] print = new int[size];
for (int i = 0; i < size; i++) {
print[i] = stack.pop().val;
}
return print;
}
}
/**
* Definition for singly-linked list.
* public class ListNode {
* int val;
* ListNode next;
* ListNode(int x) { val = x; }
* }
*/
class Solution {
public int[] reversePrint(ListNode head) {
Stack<Integer> stack = new Stack<Integer>();
while (head != null) {
stack.push(head.val);
head = head.next;
}
int size = stack.size();
int[] res = new int[size];
for (int i = 0; i < size; i++) {
res[i] = stack.pop();
}
return res;
}
}
7 反转链表
定义一个函数,输入一个链表的头节点,反转该链表并输出反转后链表的头节点。
示例:
输入: 1->2->3->4->5->NULL
输出: 5->4->3->2->1->NULL
class Solution {
public ListNode reverseList(ListNode head) {
//申请节点,pre和 cur,pre指向null
ListNode pre = null;
ListNode cur = head;
ListNode tmp = null;
while(cur!=null) {
//记录当前节点的下一个节点
tmp = cur.next;
//然后将当前节点指向pre
cur.next = pre;
//pre和cur节点都前进一位
pre = cur;
cur = tmp;
}
return pre;
}
}