【数据结构】常见单链表测验题

939 阅读6分钟

单链表倒转 和 单链表奇偶节点分离

何为单链表,一般指单向链表,由单向的节点组成,也就是节点下只有一个 next 方向属性的,单链表只能单方向遍历访问。

相对的,双向链表的双向节点一般有 beforeafter 属性,可以从前往后遍历也可以从后往前遍历。

单链表最关键的就是子节点的 next 指向,若指不好,链表可能断裂;也可能会形成一个环,在遍历访问的时候,出现栈溢出等未知错误情况。

日常开发的时候,在完成开发后,非常有必要写些单元测试,验证单链表操作的准确性!

来看下面经典的数据结构题吧,开阔一下链表思路;

定一个简单的节点类,下面的示例代码都基于这个数据模型:

class Node{
	int val;
	public Node next;
	public Node(int val){
		this.val = val;
	}
}

1. 单向链表倒转

已知一个单向链表,将其的顺序倒转,须考虑最优的时间复杂度空间复杂度。 示例如下(长度为8的一个单链表):

1 —> 2 —> 3 —> 4 —> 5 —> 6 —> 7 —> 8 —> null

链表头为节点 1

倒转后为:

null <— 1 <— 2 <— 3 <— 4 <— 5 <— 6 <— 7 <— 8

链表头为节点 8

1.1 解法分析:

  • 使用栈

    之前想过,先把链表遍历后放入一个 栈中,因为栈有先进后出的特性,再遍历栈,把栈 pop 出的节点依次重新指向,得到一个反序的链表;但是需要新建一个栈,会有额外的O(n)空间复杂度;

  • 原地算法:

    修改附近节点的指向,一次循环涉及 3 个节点,这边暂且称之为

    在未倒转前,这三个节点指向为: 前 -> 中 -> 后。也就是 前.next == 中中.next == 后 均为 true。

    在每次遍历中,需要改变的指向仅是:改变的指向,把指向。先不管 的指向,因为本次遍历的 其实就是下次遍历时候的 ,下次遍历自然会处理它的指向。

    在每次遍历中,修改完的指向后,我们还需要修改 前 中 后 的赋值,准备下一轮的遍历;也就是:

    // 注意,这边有顺序要求;
    前 = 中;
    中 = 后;
    后 = 后.next;
    
    1. 初始状态:把当前节点作为 中 节点,也就是中 = 1 ,后 = 2。没有谁是指向中的,那么,前 = null;
    2. 第一次访问:中 —> 前 : 1 由原来的指向 2 ,变为 指向 null; 准备下次访问赋值:前 = 1,中 = 2, 后 = 3
    3. 第二次访问:中 —> 前 : 2 指向 1 ;准备下次访问赋值: 前 = 2,中 = 3,后 = 4
    4. 第三次访问:中 —> 前 : 3 指向 2 ;准备下次访问历赋值: 前 = 3,中 = 4,后 = 5
    5. 省略...
    6. 第六次访问:中 —> 前 : 6 指向 5 ;准备下次访问赋值: 前 = 6,中 = 7,后 = 8
    7. 第七次访问:中 —> 前 : 7 指向 6 ;准备下次访问赋值: 前 = 7,中 = 8,后 = null
    8. 第八次访问:中 —> 前 : 8 指向 7 ;准备下次访问赋值: 前 = 8,中 = null,后 = null
    9. 第九次访问:没有第九次了,节点指向已经全部改完。注意:这时候, 为链表的脑袋,此链表的节点对象还是原来的节点对象,只是修改了其next指向方向;

    可见时间复杂度为 O(n),n为单链表长度;

1.2 盲眼手搓

public static Node reverse(Node head){
	
	if(head == null || head.next == null){
		return head;
	}

	Node before = null;
	Node middle = head;
	Node after = head.next;

	while(middle != null){
		// 修改 中 指向 前
	    middle.next = before;
		
	    before = middle;
	    middle = after;
	    if(after != null){
	        after = after.next;
	    }
	} 
	// 这个时候 before 为链表头,middle 和 after 都是 null
   return before;
}

Ctrl C / V 了,记得测一下,我瞎写的,不知道对不对...

2. 单向链表节点的奇偶排序

给定一个单链表,把所有的奇数节点和偶数节点分别排在一起。请注意,这里的奇数节点和偶数节点指的是节点编号的奇偶性,而不是节点的值的奇偶性。

请尝试使用原地算法完成。你的算法的空间复杂度应为 O(1),时间复杂度应为 O(nodes),nodes 为节点总数。

例如,原单链表为:

A —> B —> C —> D —> E —> F —> G —> H —> I —> null

节点奇偶排序后为:

A —> C —> E —> G —> I —> B —> D —> F —> H —> null

2.1 原地解法分析:

要实现上述排列效果,抓住两个关键点:隔位指向;奇偶拼接;

链表的头我们认为序号为 1 ,从字母 A(第1个)开始到字母 I (第9个)

  1. 隔位指向:

    以A为起点,进行隔位指向,A 指 C,C 指 E ... 。顺序变成: A C E G I 。这是奇数序号的重排链表。

    同时再起一个头。以 B 为起点,新建一个临时引用 Temp,赋值 B,后面链表拼接的时候会用到。再进行隔位指向,顺序变为 B D F H。这是偶数序号的重排链表。

    注意:上述的奇偶隔位指向是同时进行的,并不是先指完奇数序列再指偶数序列。

  2. 奇偶拼接:

    在遍历结束的时候,奇数的指针在 I 上, 偶数的指针在 H 上,怎么把 I 跟 B 拼接起来呢。之前建了一个临时引用 Temp ,它就是 B 节点。再将 I 指向 Temp,也就是 I 指向了 B。

2.1.1 遍历过程:

遍历流程

2.2 手撸代码:

更多的访问细节,补充在注释中:

public static Node oddEven(Node head) {
	// 防御性编程,刷一波啊
	if(head == null || head.next == null){
	    return head;
	}
	//奇数序号链表
	Node odd = head;
	//偶数序号链表
	Node even = head.next;
	//新建 temp 引用,赋值偶数序号链表的头,用于最后拼接;
	Node temp = even;
			
	// 使用下次访问的奇偶序号 作为判空条件,目的是保证下次奇偶序号必须存在;
	// 循环内已完成当前奇偶序号的指向下次,下次为空不管;
	while(odd.next != null && even.next != null){
		//隔位指向:当前奇数指向下个奇数序号,下个奇数序号在当前偶数序号的后面;
		odd.next = even.next;
		//更新下次访问的奇数序号
		odd = odd.next;
		
		// 现在的情况是:当前偶数 -> 下次奇数 —> 下次偶数;
		// 隔位指向:当前偶数序号指向下个偶数序号。
		even.next = odd.next;
		// 更新下次访问的偶数序号
		even = even.next;
	    
	}
    // 奇偶拼接
    odd.next = temp;
    // 各节点的指向已经更新完毕!
    // 返回 head,head 节点至始至终都未被赋值,还是最初的头节点;
    return head;
}

下期接着聊:排序双响炮:快排 & 归并

招聘广告 🐂

【优酷】杭州团队,长期招聘!!!

   - 前端「急」
   - Java 后端 「爆」
   - 移动端:安卓 & iOS 「热」

办公地点:蚂蚁Z空间。

面试方式:电话&视频优先。

主要有优酷少儿、创新项目等业务,P6/P7 都有。

想试一试的小伙伴,邮件联系 hdtpjhz@163.com