目录
Hello,大家好!我是Node_Hao,今天给大家带来的内容是顺序表和链表的快速入门,旨在掌握顺序表和链表的底层构造方法,以达到熟练掌握并能在leetcode独立刷题为主要目的.希望我的内容能给大家带来帮助!
线性表(linear list)是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串...
线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。
顺序表是用一段物理地址连续的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。
顺序表一般可以分为:
静态顺序表:使用定长数组存储。
动态顺序表:使用动态开辟的数组存储。
静态顺序表适用于确定知道需要存多少数据的场景.
静态顺序表的定长数组导致N定大了,空间开多了浪费,开少了不够用.
相比之下动态顺序表更灵活, 根据需要动态的分配空间大小.
明白顺序表的定义之后,我们通过实现顺序表的所有功能来从底层了解顺序表,就目前来看这的确是学习数据结构最有效的不二法门.在实现各种底层方法时我们先搭建好顺序表所需的所有类和方法.
学习数组的时候我们都知道,数组并不是一个面向对象的概念,基于这个特性它无法包含一些操作方法,因而才有了顺序表这个概念.在我看来顺序表,就是把数组面向对象赋予它操作方法,方便使用者去操作.首先我们构造一个My_ArrayList{}类,并在类中阐明顺序表所需的基本单元,也就是元素个数usedSize和顺序表长度int[]elem.最后我们定义一个构造方法用来初始化顺序表长度即可,这里大家可能不太理解usedSize的作用,现在顺序表为空,如果我往顺序表放入一个元素,那么usedSize就会++.
- public class My_ArrayList {
- public int[]elem;
- public int usedSize;
-
- public My_ArrayList() {
- this.elem = new int[10];
- }
- }
搭建好顺序表的基本类后我们就可以逐步实现其操作方法了 !
在实现复杂功能之前,我们先得完成一些辅助功能以方便后续功能的实现.
1.打印顺序表:
这时usedSize的作用就体现出来了,usedSize为多少我们就打印几个元素.
- // 打印顺序表
- public void display() {
- for (int i = 0; i < usedSize; i++) {
- System.out.println(this.elem[i] + "");
- }
- }
2.判断输入位置是否合法?
- //判断pos位置是否合法
- public boolean legal_Pos(int pos){
- if (pos < 0 || pos > usedSize) {
- System.out.println("输入位置不合法");
- return true;
- }else
- return false;
- }
3.判断顺序表是否为空?
- //判断是否为空
- public boolean isEmpty(){
- if (usedSize==0) {
- System.out.println("元素为空");
- return true;
- }else {
- return false;
- }
- }
4.判断顺序表是否满了?
- boolean isFull(){
- return this.elem.length==this.usedSize;
- }
5.获取pos位置元素
- // 获取 pos 位置的元素
- public int getPos(int pos) {
- if (legal_Pos(pos)){
- return -1;
- }
- return elem[pos];
- }
6. 查找某个元素对应位置
- // 查找某个元素对应的位置
- public int search(int toFind) {
- for (int i = 0; i < usedSize; i++) {
- if (toFind==this.elem[i]){
- return i;
- }
- }
- return -1;
- }
7.判断是否包含某个元素?(删除时用到)
- // 判定是否包含某个元素
- public boolean contains(int toFind) {
- for (int i = 0; i < usedSize; i++) {
- if(elem[i]==toFind){
- return true;
- }
- }
- return false;
- }
完成以上方法后我们就可实现其具体内容了!
1.增加元素
先判断输入位置的合法性,再判断顺序表是否满了?如果满了需要扩容这里我们采用二倍扩容.如果没有满我们先将pos位置以及pos位置之后的元素统一向后挪动一个单位,这时pos位置空出,我们再将pos位置赋值为我们想要的元素,一定不要忘记usedSize++.
- public void add(int pos, int data) {
- if(legal_Pos(pos)){
- return ;
- }
- if (isFull()) {
- this.elem = Arrays.copyOf(this.elem,2*this.elem.length);
- }
- for (int i = usedSize - 1; i >= pos; i++) {
- elem[i] = elem[i - 1];
- }
- elem[pos]=data;
- usedSize++;
- }
2.删除元素
先判断顺序表是否为空,再判断要删除的元素是否存在,如果存在记录其下标.再将其下标之后的元素统一向前挪动一个单位,这样就可以将其覆盖从而达到删除的效果了.如果想要删除全部的元素只需加个循环语句.
- //删除第一次出现的关键字key
- public void remove(int toRemove) {
- int key = 0;
- if(isEmpty()){
- return;
- }
- if(contains(toRemove)){
- key = search(toRemove);
- }else {
- System.out.println("没有你要找的元素");
- }
-
- for (int i = key; i
1 ; i++) { - elem[i] = elem[i+1];
- }
- usedSize--;
- }
3. 清空顺序表
由于顺序表中的元素都是数据类型,不包含复杂的引用所以我们清空的方式可以暴力一些.
- // 清空顺序表
- public void clear() {
- usedSize=0;
- }
1. 顺序表中间/头部的插入删除,时间复杂度为O(N)
2. 增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
3. 增容一般是呈2倍的增长,势必会有一定的空间浪费。例如当前容量为100,满了以后增容到200,我们再继续插入了5个数据,后面没有数据插入了,那么就浪费了95个数据空间。
既然存在以上问题那就说明顺序表虽然利于查询,但还是有着不足之处,而这部分法不足之处就靠链表来完善了.
链表是一种物理存储结构上非连续存储结构,数据元素的逻辑顺序是通过链表中的引用链接次序实现的 。链表的头结点极其重要,因为整个链表通常情况下靠头结点来维护.
实际情况中链表多种多样,以下情况组合起来就有8种链表结构:
1.单项 双向
2.带头 不带头
3.循环 非循环
虽然有这么多的链表结构但我们重点掌握2种,为什么?因为这两种最难,面试笔试最常考.
1.无头单向非循环链表:结构简单,一般不会单独用来存数据。实际中更多是作为其他数据结构的子结构,如哈希桶、图的邻接表等等。另外这种结构在笔试面试中出现很多.
2.无头双向链表:在Java的集合框架库中LinkedList底层实现就是无头双向循环链表
无头不代表没有头结点,只是没有一个虚拟的头结点,由下图我们可以看出其中区别.
注意:双向链表不是本章讲解重点,后续专门有双向链表的讲解.
链表不同于顺序表,它在内存中的分布并不是连续的.基于这一特性,我们需专门构造一个节点类和一个链表类,节点类用于存放节点的属性如:数据域和指针域.而链表类用于将各个节点连接,并定义其各种操作方法.
首先我们在创建好的节点类中,定义其数据域,指针域并构造初始化数据的方法.接着在链表类中定义一个头结点,然后构建一个方法用一种较为粗暴的方式枚举出链表.当然后续我们会改进链表的创建方法.构建好这些后,我们创建一个测试类,最简单的链表就实现了.
- class ListNode{
- public int val;
- public ListNode next;
- public ListNode(int val){
- this.val = val;
- }
- }
- public class My_LinkedList {
- public ListNode head;
-
- public void createList() {
- ListNode listNode1 = new ListNode(12);
- ListNode listNode2 = new ListNode(23);
- ListNode listNode3 = new ListNode(34);
- ListNode listNode4 = new ListNode(45);
- ListNode listNode5 = new ListNode(56);
- ListNode listNode6 = new ListNode(23);
- listNode1.next = listNode2;
- listNode2.next = listNode3;
- listNode3.next = listNode4;
- listNode4.next = listNode5;
- listNode5.next = listNode6;
- this.head = listNode1;
- }
- }
- public class demo3 {
- public static void main(String[] args) {
- My_LinkedList my_linkedList = new My_LinkedList();
- my_linkedList.createList();
- }
当然我们这时还看不到链表,想要看到链表得先在链表类中实现display()方法.
1.打印链表:
因为链表的头不能动,链表靠头结点来维护,所以我们创建一个节点,遍历一遍链表即可,不过要注意链表没有++的概念,想要后移必须 cur = cur.next;
- //打印链表
- public void display() {
- ListNode cur = head;
- while (cur != null) {
- System.out.print(cur.val + " ");
- cur = cur.next;
- }
- }
2.获取链表的长度:
思想与打印链表一致,不过是加了一个加法器count.
- //得到单链表的长度
- public int size() {
- ListNode cur = head;
- int count = 0;
- while (cur != null) {
- count++;
- cur = cur.next;
- }
- return count;
- }
3.返回Index位置的元素:
假设链表长度为4,打印第3位的元素,那么cur只需走两步即可.
- //返回Index位置的元素
- public ListNode FindIndex(int index) {
- ListNode cur = head;
- while (index - 1 != 0) {
- cur = cur.next;
- }
- return cur;
- }
4.查找关键字key是否在链表中:
- //查找是否包含关键字key是否在单链表当中
- public boolean contains(int key) {
- ListNode cur = head;
- while (cur != null) {
- if (cur.val == key) {
- return true;
- }
- cur = cur.next;
- }
- return false;
- }
以上都是辅助操作,接下来才是链表的真正实用功能!
1.头部插入:
创建一个节点让其指向head,然后把head前移一位即可.
- //头插法
- public void addFirst(int data) {
- ListNode node = new ListNode(data);
- node.next = head;
- head = node;
- }
2.尾部插入:
创建一个节点,判断链表是否为空如果为空直接等于头结点即可.如果不为空再定义一个ListNode类型的变量cur,让cur遍历一遍链表找到最后一位,然后把最后一位指向node即可.这里大家不必纠结为什么node最后没有置为空,创建结点类时指针域没有赋值,默认为null.
- //尾插法
- public void addLast(int data) {
- ListNode node = new ListNode(data);
- if (head == null) {
- head = node;
- } else {
- ListNode cur = head;
- while (cur.next != null) {
- cur = cur.next;
- }
- cur.next = node;
- }
- }
3.任意位置插入:
首先判断输入位置是否合法,如果插入0位置调用头插法即可,如果插入最后一位调用尾插即可.如果上述三种情况都不符合调用我们之前写好的返回index位置的元素的方法,接收到要插入的位置,然后创建一个节点node插入即可.(cur指向的位置变为node指向,再让cur指向node)
- //任意位置插入,第一个数据节点为0号下标
- public void addIndex(int index, int data) {
- if (index < 0 || index > size()) {
- System.out.println("输入位置不合法 ");
- return;
- }
- if (index == 0) {
- addFirst(data);
- return;
- }
- if (index == size()) {
- addLast(data);
- return;
- }
- ListNode cur = FindIndex(index);
- ListNode node = new ListNode(data);
- node.next = cur.next;
- cur.next = node;
- return;
- }
4.找到前驱,为删除操作做准备:
当我们要删除一个元素时,一定要知道它前面的元素.也就是我们常说的前驱
- //找到前驱
- public ListNode searchPrev(int key) {
- ListNode cur = head;
- while (cur.next != null) {
- if (cur.next.val == key) {
- return cur;
- }
- cur = cur.next;
- }
- return null;
- }
5.删除第一次出现的关键字为key的节点:
首先单链表不能为空,如果没有意义了,其次如果要删除head节点,直接把head向后移一位即可.如果不是以上两种情况,那么我们调用之前写过的找前驱的函数,找到要删除节点的前驱,(如果返回的前驱为0,说明输入的的节点不存在)然del指向的位置变成cur指向即可.
- //删除第一次出现关键字为key的节点
- public void remove(int key) {
- if (head == null) {
- System.out.println("单链表为空不能删除!");
- return;
- }
- if (head.val == key) {//删除头结点
- head = head.next;
- return;
- }
- ListNode cur = searchPrev(key);
- if (cur == null) {
- System.out.println("删除的节点不存在");
- return;
- }
- ListNode del = cur.next;
- cur.next = del.next;
- }
6.删除所有关键字为key的节点:(力扣原题)
首先还是判断链表是否为空,这时如果要删头结点我们不能直接删,因为头结点后一个也可能要删除,所以头结点我们留到最后删.其次我们定义一个前驱prev和一个cur,假设我们只定义一个cur,一但遇到多个需要删的元素挨在一起根本就无从下手.所以我们将prev=head,cur = head.next.此时prev就像个掌握生杀大权的长官,而cur就像一个士兵一样不停的探路.当cur!=key时说明安全,prev就向后走一位.当cur==key时说明危险,prev就不走了直接指向cur的下一位.
- //删除所有值为key的节点!!上升到面试笔试的难度
- public ListNode removeAllKey(int key) {
- if (head == null) {
- System.out.println("单链表不能为空!");
- return null;
- }
- ListNode prev = head;
- ListNode cur = head.next;
- while (cur != null) {
- if (cur.val == key) {
- prev.next = cur.next;
- cur = cur.next;
- } else {
- prev = cur;
- cur = cur.next;
- }
- }
- if (head.val == key) {
- head = head.next;
- }
- return this.head;
- }
7.清空链表:
如果我们直接拿head置空,会发现无法向后走,所以定义curNode记录head的后一位让其向后走.
- public void clear() {
- while (head != null) {
- ListNode curNext = head.next;
- head.next = null;
- head = curNext;
- }
- }
以上就是快速入门顺序表和链表的全部内容了,希望我通俗易通的讲解能够帮到你!后续会推出链表的面试笔试题已经双向链表,敬请期待!