数据结构专题(一)——数组与链表

数组与链表是计算机内存存储方式的基石,理解了他们对后续的数据结构以及算法学习大有好处,比如把链表的增删方式做一些限制就能形成所谓的队列和栈结构,并且链表在图与树结构的存储中也大有用处。

学习数据结构与算法的关键就是要将学到的知识用起来,通过解决问题来不断进步,加深对于数据结构与算法的理解。网络上也有许多关于链表的操作问题,主要有如下几个:

1. 单链表的创建和遍历

2. 从尾到头打印单链表

3. 求单链表中节点的个数

4. 查找单链表中的倒数第k个节点

5. 查找单链表中的中间节点

6. 反转链表

7. 合并两个有序的单链表,合并之后的链表依然有序

8. 删除链表节点

因为链表是一个非常有用的数据结构,正因为它的存在才有后续的栈,队列等数据结构,包括操作系统的一部分功能也是由链表来进行实现的。它克服了数组存储空间连续的限制,在增加与删除元素操作上效率也高于数组,所以说链表是计算机科学家们智慧的结晶。由于链表的广泛使用,计算机编程语言自然也就少不了对于链表数据结构的支持,很多高级语言都有专门的库去实现链表。链表的形态也是非常灵活多变的,实际开发中不仅有单链表,还存在双链表,单向循环链表以及双向循环链表等,需要读者先自己去探索了。这里就采用Java实现单链表的基本操作。

1. 单链表的创建和遍历

首先我们要善于运用Java面向对象的思想,单链表每个节点的属性都是相同的,所以我们可以把节点抽象出来,形成一个对象,这个对象就像一个小盒子,里面开辟了两个空间,一块用来存储数据,另一块用来存储下一个节点的地址。(下图来自维基百科)

那么在 Java中我们可以创建一个链表节点对象,具有两个属性,一个是存储数据,还有一个就是存储下一个节点。

class Node<T>{
	
    T data;
    Node<T> next;
    
    Node(T data){
        this.data = data;
        this.next = null;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }

    public Node<T> getNext() {
        return next;
    }

    public void setNext(Node<T> next) {
        this.next = next;
    }
    
}

在创立完节点对象后,下一步就是将节点连接起来形成链表。若我要创建存储整数对象的链表,代码如下。

Node<Integer> node1 = new Node<Integer>(10);
Node<Integer> node2 = new Node<Integer>(20);
Node<Integer> node3 = new Node<Integer>(30);
		
node1.next = node2;
node2.next = node3;

接下来就是遍历单链表的操作了,说到遍历有两种方式,分别为正向遍历与反向遍历。先来看看正向遍历,正向遍历的思维方式就是

(1) 节点不是空节点时,执行(2),否则跳出循环
(2)输出该节点的数据
(3)通过该节点存储的地址去寻找下一个节点,并将下一个节点赋值给当前节点
(4)回到步骤(1)

代码实现我想读者们肯定有思路,看到“当”这个条件,自然而然就会想到while循环。

while(node != null){
    System.out.print(node.data);
    node = node.next;
}

可见正向遍历的逻辑是非常清晰的,通过逐个节点扫描,输出节点数据即可。接下来就是逆向遍历了,这就需要一定的思维深度。

关于逆向遍历,可以用来求解第二个问题。

2. 从尾到头打印单链表

其实说到逆向遍历,我们需要首先找到链表的最后一个节点,那就从头节点逐层深入到尾节点,然后再执行输出操作,并逐步返回上一层,直到回到头节点。具体思维结构如下图,这种思维结构可以理解为一种递归。

一说到递归,很多人肯定头疼不已,那是因为没有将问题抽象,记得看到过一篇关于递归的解释:“千万不要深入递归函数中去看问题。” 递归是一种思维方式,是把大问题分解成小问题的方式,如上的思维可以理解为“我只想从头节点不断向后找节点,直到找到该链表的尾部,然后从最后逆向输出每个节点的值罢了”,什么时候停止寻找呢?那就是当你发现你找到的节点后面没有其他节点了,那么你所在的位置就是该链表最后的节点了。具体代码实现如下。

public static void FindTail(Node<T> node) {
	if(node != null) {
	    FindTail(node.next);
	    System.out.print(node.data);	
	}
}

通过递归的思考,逆向遍历的问题解决了,递归其实是比较消耗计算机资源的,还有许多有创意的遍历方法,希望读者们自己进一步去探索。

3. 求单链表中节点的个数

既然我们已经学会遍历链表了,那么就可以在遍历过程中设置计数器来统计链表节点的个数。具体实现只要设置一个临时变量存储统计的值即可。代码实现如下。

int temp = 0;

while(node != null){
    System.out.print(node.data);
    temp++;
    node = node.next;
}

System.out.println(temp);

4. 查找单链表中的倒数第k个节点

既然学会了遍历,那就可以把遍历的结果存储到动态数组ArrayList里,就可以解决该问题。

List<T> list = new ArrayList<T>();

while(node != null){
    list.add(node.data);
    node = node.next;
}

ans = list.get(list.size() - k);

5. 查找单链表中的中间节点

若没有遍历次数以及空间的限制,那么可以遍历一遍链表并且把结果存到数组中进行操作。但是如果只能遍历一次并且不能使用新的存储空间的话可以使用双指针方法。指针p对节点逐个遍历,而指针q则是间隔为2进行遍历,当指针q位于链表的末尾时,指针p则为链表的中间节点,具体算法如下。

public static int getMiddle(Node<Integer> node) {
		
	Node<Integer> p = node;
	Node<Integer> q = node;
		
	while(q != null && q.next != null && q.next.next != null) {
	    p = p.next;
	    q = q.next.next;
	}	
	return p.data;
}

6. 反转链表

反转链表的方法最容易想到的就是设立一个临时节点去存储当前节点,然后将当前节点的指针方向进行修改即可。通过循环遍历链表,对每个节点都进行临时存储与调整指针。

public static Node<Integer> reverse(Node<Integer> head) {
		
	if(head == null) {
	    return head;
	}

	Node<Integer> preNode = head;
	Node<Integer> currentNode = head.getNext();
	Node<Integer> temp = null;
	
	while(currentNode != null) {
	    temp = currentNode.getNext();
	    currentNode.setNext(preNode);
	
	    //将currentNode赋值给preNode本质是将指针位向后移动了一位
	    //例子:在第一次循环中把原本的当前节点位变成了第二次循环中的前节点位
	    preNode = currentNode;
	    currentNode = temp;
	}
	//将头节点的指向变为空从而形成末节点
	head.setNext(null);
	return preNode;
}

7. 合并两个有序的单链表,合并之后的链表依然有序

如何合并两个有序的单链表呢?首先我们应该庆幸给的链表本来就是有顺序的,那么一种办法就是分别将两个链表中对应的元素进行比较。经过研究发现,其实我们合并单链表的操作是重复的,就是在进行遍历比较,具体的逻辑结构如下图所示。也就是说把合并问题转化为同类型的子问题,这就是递归!

如果我们使用递归对该问题进行求解,代码会非常简洁。

//假设需要合并的这两个链表的长度是一样的
public static Node<Integer> merge(Node<Integer> n1, Node<Integer> n2){
	
	if(n1 == null) {
	    return n2;
	}
	if(n2 == null) {
	    return n1;
	}
	
	Node<Integer> head = null;
	
	if(n2.data >= n1.data) {
	    head = n1;
	    head.next = merge(n1.next, n2);
		
	}else {
	    head = n2;
	    head.next = merge(n1, n2.next);
	}
	
	return head;
}

8. 删除链表节点

终于来到了最后一个问题,链表的增加与删除效率是非常高的,因为只要调整两个指针的指向即可,而不需要像数组这样为了给新元素提供存储空间,移动很多元素。所以当项目涉及到很多的增加与删除操作时,可以考虑使用链表提高系统的运行效率。

public static void deleteNode(Node<Integer> delNode) {
		
    if(delNode == null || delNode.next == null) {
        return;
    }else {
        Node<Integer> temp = delNode.next;
        delNode.data = temp.data;
        delNode.next = temp.next;
    }
}

总结:写了这么多,发现链表是一个非常灵活的数据结构,涉及的知识点也很多,刚接触的时候肯定是晕乎乎的,不过经过以上的问题解答,链表的各项操作基本都可以从以上的几个问题变形出来,一直比较相信通过问题来锻炼自己对于知识的掌握,希望能多加练习,从而彻底攻克链表:>

 

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值