【Java数据结构】通过Java理解和实现——顺序表和单链表
🍉线性表
线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串…
线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。
🍉顺序表
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。
🌵顺序表概念及结构
顺序表一般可以分为:
静态顺序表:使用定长数组存储。
动态顺序表:使用动态开辟的数组存储。
静态顺序表适用于确定知道需要存多少数据的场景.
静态顺序表的定长数组导致N定大了,空间开多了浪费,开少了不够用
相比之下动态顺序表更灵活, 根据需要动态的分配空间大小.
接下来详细讲解动态顺序表的实现
🍌顺序表接口实现(注释非常详细,我👴👴都能看懂)
先把这个顺序表类的成员属性和构造函数写好
然后就是实现接下来的一个个接口
🍈打印顺序表
这里很简单,就像遍历数组一样,把顺序表遍历一遍即可
// 打印顺序表 public void display() { for (int i = 0; i < this.usedSize; i++) {//将有效元素遍历并且打印 System.out.print(this.elem[i]+" "); } System.out.println(); }
🍈在pos位置新增元素
这里思路分为以下几个步骤:
- ①判断pos是否合法
- ②判断顺序表满没满(这里还>需要额外写一个判断方法isFull()),满了需要Arrays.copyOf()扩容
- ③pos之后的元素依次向后移动个位置
- ④将目标元素data放入这个pos位置
//判断顺序表是否满了 public boolean isFull() {//判断方法就是将有效元素个数和数组长度比较 if (usedSize==elem.length)//如果两者相等,说明这个数组已经装满了 return true; else return false; } // 在 pos 位置新增元素 public void add(int pos, int data) { if (pos>=0 && pos<=usedSize){ //首先保证pos得是合法的 if (isFull()){//判断容量是否已经满了 this.elem = Arrays.copyOf(this.elem,this.elem.length+1);//满了得扩容 } for (int i = usedSize-1 ; i >= pos ; i--){ //要从后往前移动,不能从前往后移,这样前一个位置的值移到后一个位置的值上, //那后一个位置上的值就会被覆盖,导致后边的所有值都变成与第一个移动的值一样 elem[i+1] = elem[i];//从新增元素位置之后的所有元素整体往后移动 1位 } this.elem[pos] = data;//向pos位置插入数据 this.usedSize++;//有效元素+1 } else System.out.println("位置不合法"); }
🍈获取顺序表长度
这个很简单,获取成员属性的有效长度useSize就可以了
// 获取顺序表的有效数据长度 public int size() { return this.usedSize; }
🍈判断是否包含某个元素
传入需要查找的元素,然后在所有有效元素中依次查找
// 判定是否包含某个元素 public boolean contains(int toFind) { for (int i =0 ; i<usedSize ; i++){//在有效元素里依次查找 if (elem[i]==toFind) return true; } return false; }
🍈查找某个元素对应的位置
这里可以用遍历有效元素就好了和上边一样,我只不过想复习一下二分查找,因为二分查找虽然也是一种遍历,但是可以很好的提高遍历的效率
// 查找某个元素对应的位置(复习一遍二分查找) public int search(int toFind) { int left = 0; //设置一个左下标 int right = elem.length-1;//再设置一个右下标,值为数组长度-1 while (left <= right) {//循环查找,条件就是左下标<=右下标 //=号不要忘,不然会漏掉一种情况(所查值为最后一个元素的情况) int mid=(left+right)/2;//设置中间值,用来减半需要遍历的元素,提高效率 if (elem[mid]<toFind){//如果这个中间值小于目标值,就把这个中间值的下一个元素设为左值 left = mid+1; }//与上边同理 else if (elem[mid]>toFind){ right = mid-1; } else//如果这个中间值既不大于也不小于目标值,那这个中间值就是所要查找目标值,返回其下标就好了 return mid; } System.out.println("没有这个数"); return -1; }
🍈获取pos位置的元素
传入一个位置,先判断这个位置是否合法,合法的话直接返回这个位置上的元素就好了
// 获取 pos 位置的元素 public int getPos(int pos) { if (pos>=0 && pos<=usedSize)//判断位置是否合法 return elem[pos]; else return -1;//如果位置不合法返回-1表示位置不合法 }
🍈给pos位置的元素设为value
依旧先判断位置是否合法,合法的话,直接将value赋值给这个位置覆盖掉原数据
// 给 pos 位置的元素设为 value public void setPos(int pos, int value) {//传入位置和想赋予的值 if (pos>=0 && pos<=usedSize){//判断是否合法 elem[pos]=value; } else System.out.println("pos非法"); }
🍈删除第一次出现的数据
先调用上边已经写好的查找接口判断是否有这个数据,如果有的话,就从此数据开始依次将当前数据的下一个数据覆盖当前数据实现删除功能不要忘了有效元素-1
//删除第一次出现的关键字key public void remove(int toRemove) { if (-1==this.search(toRemove)){ System.out.println("没有这个元素"); } else{ int index = this.search(toRemove);//获得要删除数据的位置(下标) for (int i = index ; i<usedSize-1 ; i++)//从此数据开始依次将当前数据的下一个数据覆盖当前数据实现删除功能 elem[i]=elem[i+1]; usedSize--;//注意删除一个元素之后,整个顺序表的有效元素也要-1嗷 } }
🍈清空顺序表
这里就用最粗暴的办法,直接将有效元素个数清零,其实不用管数组元素是否都变成0,因为下一次使用的时候又会有新的数据覆盖上去
// 清空顺序表 public void clear() { this.usedSize = 0; }
🍌顺序表的缺陷
- 1. 顺序表中间/头部的插入删除,时间复杂度为O(N)
- 2. 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
- 3. 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。
链表会不会存在以上的问题呢?请往下看👇👇
🍉链表
🌵链表概念及结构
链表是一种物理存储结构上非连续存储结构,数据元素的逻辑顺序是通过链表中的引用链接次序实现的 。
链表结构非常多样,以下情况组合起来有8种链表结构:
- 单向、双向
- 带头、不带头
- 循环、非循环
我们重点学习以下两种链表
🍌无头单向非循环链表接口实现(注释非常详细,我👴👴都能看懂)
先写两个类,一个是链表类(包含有一个可变的头结点和实现各种功能的接口,因为是无头链表,所以这个头结点是可变的),一个是节点类(成员属性有value和next)
接下来的接口都是写在链表类里的,因为链表是一个对象,我们要实现的就是一个链表对象所有的功能
🍈打印链表
打印链表其实和打印顺序表类似,遍历链表就好了,不过要注意一个点,这里需要引入一个局部变量cur来代替头节点去遍历,因为头结点在没添加或者删除节点之前是固定的,不要让头结点变来变去
//打印链表 public void display(){ ListNode cur = this.head;//利用创建一个局部变量cur来替代head,这样头结点就不会改变了 while (cur!=null) {//遍历的条件就是下一个节点的引用(地址)不为null System.out.print(cur.value+" "); cur = cur.next;//找到下一个节点 } System.out.println(); }
🍈头插法插入
顾名思义,头插法就是从头部插入节点,使新创建的节点成为新的头结点,这里需要额外考虑一个点,就是头结点是否存在(链表是否为空),不过以下代码能处理头结点为空的情况(注意要先将原头结点的地址先存入新节点,再将新节点引用赋给原头结点成为新头结点)
//头插法 public void addFirst(int data){ ListNode node = new ListNode(data);//创建新节点并初始化节点的data node.next = this.head;//将当前头结点地址存到新节点的next里 this.head = node;//将新创建的节点变为头结点 //这两行代码包含了头结点为null的情况 }
🍈尾插法插入
尾插法不同于头插法,必须先判断链表是否为空(判断头结点是否为null),然后引入局部变量cur遍历链表,直到cur.next为空的时候,说明找到尾节点了,此时的cur就是尾巴结点
//尾插法 public void addLast(int data){ //找尾巴,cur.next为null了,说明这是尾节点了 ListNode node = new ListNode(data);//创建新节点并初始化节点的data if (this.head == null){ //尾插的第一次必须判断头结点是否为空 this.head = node;//如果是第一次插入,则新节点就是头结点 } ListNode cur = this.head; //引入局部变量cur遍历链表 while(cur.next != null){ //next等于null了就跳出while了 cur = cur.next;//找到下一个节点 } cur.next = node; }
🍈查找是否包含关键字key在单链表当中
传入关键字key,依旧引入局部变量cur遍历链表,哪个节点的value等于key了,说明链表里有这个关键字,返回true,否则返回false
//查找是否包含关键字key是否在单链表当中 public boolean contains(int key){ ListNode cur = this.head;//引入局部变量cur遍历 while(cur != null){//循环条件为节点引用不为null if (key == cur.value) return true;//如果找到了,返回true cur = cur.next;//找到下一个节点 } return false; }
🍈得到单链表的长度
依旧采取引用局部变量cur来遍历链表,还要多设置一个局部变量size来计数,只要节点不为null,size就+1,最后返回size的值就是链表长度了
//得到单链表的长度 public int size(){ int size=0;//引入局部变量来计数 ListNode cur = this.head; while(cur != null){//遍历并计数 size++;//节点不为null,计数器+1 cur = cur.next;//找到下一个节点 } return size;//返回链表长度 }
🍈任意位置插入,第一个数据节点为0号下标
首先得判断你要插入的这个位置是否合法,然后这里需要额外写一个查找插入位置前一个节点的方法findIndex()用于插入节点,插入原理
//根据传入的index查找前一个节点并返回地址 public ListNode findIndex(int index){ ListNode cur = this.head;//引入局部变量遍历至index前一节点 while (index-1 != 0){//停止条件就是index-1等于0了 //也就是说遍历到index位置上一个节点了 cur = cur.next;//向后遍历 index--;//每向后找一个节点index减1 } return cur;//返回index上一个节点引用 } //任意位置插入,第一个数据节点为0号下标 public void addIndex(int index,int data){//需要创建一个查找index位置前一节点的函数 if (index > 0 && index < size()) {//判断插入位置是否合法 ListNode node = new ListNode(data);//创建新节点并初始化节点的data node.next = findIndex(index).next;//通过上边写的查找方法将index位置前一节点的下一节点引用赋给新插入节点的next findIndex(index).next = node;//将新节点的引用存到查找到的节点的next达到链接效果 } else if (index==0) {//如果插入位置是0,则直接使用头插法插入 addFirst(data); return; } else if(index==size()) {//如果插入位置是链表长度值,则直接使用尾插法插入 addLast(data); return; } else System.out.println("位置不合法!"); return; }
🍈删除第一次出现关键字为key的节点
首先判断头结点是否为null(链表是否为空),然后有两种情况
①关键字在头结点:将头结点下一个节点设置为新头结点
②关键字不在头结点:将有关键字的节点的下一节点引用赋给有关键字节点的上一节点的next//删除第一次出现关键字为key的节点 public void remove(int key){ if (this.head == null){//判断链表是否为空 System.out.println("链表为空,不能删除"); return; } ListNode cur = this.head; while(cur.next != null){//遍历链表 if(cur.value == key) {//①关键字在头结点:将头结点下一个节点设置为新头结点 head = cur.next; return; } else if(cur.next.value == key) {//②关键字不在头结点:将有关键字的节点的下一节点引用赋给有关键字节点的上一节点的next cur.next = cur.next.next; return; } cur = cur.next; } System.out.println("没有你要删除的节点"); }
🍈删除所有值为key的节点
和上边删除第一次出现的key类似,只不过return换成了continue,再有就是需要设置一个局部变量size,删除过程进行完之后若删除前设置的size值没发生改变,则说明没有删除节点
//删除所有值为key的节点 public void removeAllKey(int key){ int size = size(); if (this.head == null){ System.out.println("链表为空,不能删除"); return; } ListNode cur = this.head; while(cur.next != null){ if(cur.value == key) { head = cur.next; continue;//删除之后不要return,继续遍历 } else if(cur.next.value == key) { cur.next = cur.next.next; continue;//删除之后不要return,继续遍历 } cur = cur.next; } if(size()==size) {//如果进行删除前设置的size等于进行删除后size()方法返回的值,说明没有进行删除操作 System.out.println("没有你要删除的节点"); } }
🍈清空链表
暴力清空,直接将头结点置空,这样整个链表就无法找到了
//清空链表 public void clear(){ this.head = null; }
顺序表和链表的区别与联系
顺序表
顺序表:一白遮百丑
白:空间连续、支持随机访问
丑:1.中间或前面部分的插入删除时间复杂度O(N) 2.增容的代价比较大。
链表
链表:一(胖黑)毁所有
胖黑:以节点为单位存储,不支持随机访问
所有:1.任意位置插入删除时间复杂度为O(1) 2.没有增容问题,插入一个开辟一个空间。
🌙🌙🌙🌙🌙🌙🌙🌙🌙🌙🌙🌙🌙🌙🌙🌙🌙🌙🌙🌙🌙🌙🌙🌙🌙
❤原创不易,如有错误,欢迎评论区留言指出,感激不尽❤
❤ 如果觉得内容不错,给个三连不过分吧~ ❤
❤ 看到会回访~ ❤
🌙🌙🌙🌙🌙🌙🌙🌙🌙🌙🌙🌙🌙🌙🌙🌙🌙🌙🌙🌙🌙🌙