前言
学了数据结构之后,就用编程语言去实现它,这样理解才通透。同时地,学会刷题也是很重要,例如现在人尽皆知的leetcode。 我的目标就是进大厂,而必须要过的一关就是数据结构,慢慢学吧~ 行百里者半九十。
我实现数据结构的代码是java,用什么语言不重要,主要是思路正确就可以了。所以你想学习c、c++也是一样的。
然后,这个博客系列是给小白看的,所以我有一些阅读建议:我会给出实现思路,先跟着我一起思考,虽然有些地方说的有些拗口,没办法,这是写作的缺点之一,所以要理解每一个地方说的概念后再往下,不然发现看不懂自然而然就“收藏夹吃灰”了
看懂了不一定会,要自己实现一下,你会发现巨多的坑等着你
不要急,慢慢来,你可以学会的(真的)
上一节我们提到了链表,那么我们这一篇就来实现其结构。
链表的定义:1)链表是以节点的方式来存储是链式存储
2)每个节点包含data域(存放的数据), next域:指向下一个节点,当next指向null时,可以表明才链表元素是尾结点
单向链表
首先,我们设计一个对象Node,对象里面只含有两个字段,分别是data域和next域
并添加上getter、setter方法,这个代码很简单class Node{
private String data;
private Node next;
public Node(String data, Node next){
this.data = data;
this.next = next;
}
public String getData(){
return data;
}
public void setData(String data){
this.data = data;
}
public Node getNext(){
return next;
}
public void setNext(Node next){
this.next = next;
}
@Override
public String toString(){
return "Node{" +
"data='" + data + '\'' +
'}';
}
}
接下来,就是设计API了,一个链表应该拥有插入、删除、打印、获取链表长度等功能。
首先,我们来讨论一下主要的API的功能:
插入:
我们要实现的功能是,在i元素之前插入某个新节点。如图:
本来第 i 个节点是直接指向第 i+1 个节点的,由于来了新节点,我们就需要更改一下指向的顺序。
我们要做的具体步骤有三个:
① 找到第i个元素的节点
② 将该节点的next域指向我们传入的新节点
③ 将新节点的next域指向第 i+1 个节点
很简单,是不是?
不过,仔细一看上面这三个步骤,事情并没有那么简单。
我们想一想,如果实现了步骤②,那么到了步骤③,我们如何去表示 i 元素的 next 域呢? 当前状态下, i 元素的 next 域可是新节点噢(步骤②做的事情)。所以我们丢失了原先 i 节点的下一节点,怎么办呢
换个角度想一下,先实现步骤③就可以了。
我们先将新节点指向第 i+1个节点,然后再把 i 节点指向新节点,就可以了(链表当中,顺序很重要)。
删除
而删除的思路跟插入是差不多的,实现的效果是:传入第i个位置的上一个位置,然后删除它下一个节点,再连向下一节点的下一节点。
为什么不直接找到节点然后直接删除呢?因为该链表是单向的,没办法表示“当前节点的上一个节点”,所以我们无法让“上一个节点”与“下一个节点相连”。
所以思路是:从链表头节点一直往下找,找i -1次,就找到了所删除节点的上一节点。
好了,说完思路,我把完整代码贴出来:class singleLinkedList{
/**
* 传入一个头结点,遍历链表
*/
public void list(Node head){
Node temp = head.getNext(); // 头结点的下一节点
while (true) {
if (temp == null) {
// 找到最后节点,跳出循环
break;
}
System.out.println(temp);
temp = temp.getNext();
}
}
/**
* 传入参数i,删除第i个元素
*/
public Node delete(Node head, int i){
Node temp = head.getNext(); // 辅助节点, 指向了头结点的下一节点
for (int j = 1; j < i; j++) {
if (temp != null){
temp = temp.getNext(); // 辅助节点往后移动
}else{ // 说明temp.getNext() == null ,已经到了尾结点
System.out.println("链表长度不足"+i+",请重新传参");
return null;
}
}
// 跳出循环,表示找到要删除节点的上一节点
Node two_next = temp.getNext().getNext(); // 找到下一节点的下一节点,记为two_next
temp.getNext().setNext(null); // 将下一节点的值设置为空
temp.setNext(two_next); // 将当前节点直接指向two_next
return head;
}
/**
* 传入参数i,在i-1的元素前面插入,node是要插入的节点
*/
public void insert(Node head, int i, Node node){
Node temp = head; // 辅助节点
for (int j = 1; j < i; j++) {
if (temp == null) {
// 下一节点为空,表示链表长度不够
System.out.println("链表长度不足"+i+",请重新传参");
break;
}
else{
temp = temp.getNext();
}
}
// 跳出循环时,temp表示要插入节点位置的上一个节点
Node nextNode = temp.getNext();
node.setNext(nextNode);
temp.setNext(node);
}
/**
* 返回链表的长度
* @return
*/
public int getLength(Node head){
int count = 0;
Node temp = head.getNext();
while (temp != null){
temp = temp.getNext();
count++;
}
return count;
}
}
汗...这样搞得我好像文章很长一样,这样会不会拉到这里的人变少了很多??不过当年写爬虫文章的时候,这么长的文是常有的事,哈哈。
然后,觉得写的还可以的,点个赞让我知道你来过好吗(卑微)
环形链表
将单链表中终端结点的指针端由空指针改为指向头结点,就使整个单链表形成一个环,这种头尾相接的单链表称为单循环链表,简称循环链表( circular linked list)。
循环链表如图所示:
跟链表结构没有多大差别,只不过我们判断是否是尾结点的方式由 a.next == null 改成了 a.next == head(头结点)
当next域指向的是头结点时,我们认为当前节点为尾结点。
环形链表的好处:
当我们总是要获取最后一个节点时,可以使用环形链表。
首先实例化一个指向尾结点的变量,这意味着遍历时总是从尾结点开始找,对于前几个元素,只不过把操作数+1;而原本我们想要获取最后一个元素需要操作n次(链表节点个数的大小),而现在,有了环形链表,访问最后一个节点的操作数是1。
但是我们发现,这还是有些不够灵活,如果我们想要获取倒数第二个节点怎么办?
这时候,双向链表就起到了作用……
双向链表
双向链表,就是可以通过两个方向遍历链表,别看这图画的有些复杂,其实就是多了一个pre域(与next域相对的),用于指向上一个节点。
双向链表的优点:
双向链表相对于单链表来说,要更复杂一些,不过,双向链表使得对某个结点的前后结点的操作,带来了方便,可以有效提高算法的时间性能。说白了,就是用空间来换时间。
对于双向链表的操作,也就是多了几步,但原理是一样的。下面举个添加节点的例子表明一下:
步骤①:将node(新节点)的pre域指向temp
步骤②:新节点指向下一节点
步骤③:下一节点的上一节点指向新节点
步骤④:当前节点的下一节点指向node
嗯…好啰嗦好啰嗦,简单来说,就是多了一个pre域,但是指向域的操作顺序是很重要的,不然就发生丢失节点的情况了(参考单向链表)。
添加代码实现:
假设,现在有个新节点node,temp为想要添加位置的前一个位置节点。嗯,为了文章篇幅,这里贴出的是伪代码,你们看懂了就好啦~
if (temp.next != null){ // 如果这个节点不是尾结点
temp = node.pre; // 当前节点下一节点指向node(新节点)
node.next = temp.next; // 新节点指向下一节点
temp.next.pre = node; // 下一节点的上一节点指向新节点
node.pre = temp; // 当前节点的下一节点指向node
}else{ // 如果这个节点是尾结点
temp.next = node;
node.pre = temp;
}
写在后面
这个系列会一直写,后面的还有数组、队列、还有我还没学的二叉树等等。
对于知识点梳理的同时,又创作了一篇博客,还有各位小伙伴点的赞,这给我带来无穷大的动力。
冲冲冲!!