目录
一、线性表
线性表 ( linear list ) 是 n 个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构,常见的线性表:顺序表、链表、栈、队列、字符串... 线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物理结构上并不一定是连续的,线性表在物理上存储时,通常以数组和链式结构的形式存储。
二、顺序表
1.概念及结构
(1)顺序表是用一段 物理地址连续 的存储单元依次存储数据元素的线性结构,一般情况下采用数组存储。在数组上完成数据的增删查改。其实顺序表就是一个数组,但是为什么还要有顺序表呢?因为顺序表写到类里面,将来就可以面向对象。
(2)顺序表一般可以分为:
静态顺序表:使用定长数组存储。
动态顺序表:使用动态开辟的数组存储。
(3)静态顺序表适用于确定知道需要存多少数据的场景 . 静态顺序表的定长数组导致N定大了,空间开多了浪费,开少了不够用。相比之下动态顺序表更灵活 , 根据需要动态的分配空间大小 .
2.接口实现
我们来实现一个动态顺序表
import java.util.Arrays;
class MyArrayList{
public int[] elem;
public int usedSized;//有效数据长度 不用初始化,因为它默认为0
public MyArrayList(){
this.elem = new int[10];
}
//打印顺序表
public void display(){
for(int i = 0; i < usedSize; i++){
System.out.print(this.elem[i]);
}
System.out.println();
}
//获取顺序表有效数据长度
public int size(){
return this.usedSize;
}
//在pos位置新增元素
public void add(int pos,int data){
if(pos < 0 || pos > this.usedSize){
System.out.println("pos位置不合法!");
return;
}
if(isFull()){
this.elem = Arrays.copyOf(this.elem,2*this.elem.length);
}
for(int i = usedSize - 1; i >= pos; i--){
this.elem[i + 1] = this.elem[i];
}
this.elem[pos] = data;
this.usedSize++;
}
//判断顺序表中元素位置满不满
public boolean isFull(){
return this.usedSize == this.elem.length;
}
// 判定是否包含某个元素
public boolean contains(int toFind){
for(int i = 0; i < this.usedSize; i++){
if(this.elem[i] == toFind){
return true;
}
}
return False;
}
// 查找某个元素对应的位置
public int search(int toFind){
for(int i = 0; i < this.usedSize; i++){
if(this.elem[i] == toFind){
return i;
}
}
return -1;//因为数组下标没有-1
}
// 获取 pos 位置的元素
public int getPos(int pos) {
if(pos < 0 || pos >= this.usedSize){
System.out.println("pos位置不合法!");
return -1;//业务上的处理不考虑
}
return this.elem[pos];
}
// 给 pos 位置的元素设为 value
public void setPos(int pos, int value) {
if(pos < 0 || pos >= this.usedSize){
System.out.println("pos位置不合法!");
return;
}
this.elem[pos] = value;
}
//删除第一次出现的关键字key
public void remove(int toRemove) {
if(isEmpty()){
System.out.println("顺序表为空");
return;
}
int index = search(toRemove);
if(index = -1){
System.out.println("没有你要删除的位置");
return;
}
for(int i = index; i < this.usedSize - 1; i++){
this.elem[i] = this.elem[i+1];
}
usedSize--;
//this.elem[usedSize] = null; 如果数组当中是引用类型
}
//顺序表是否为空
public boolean isEmpty(){
return this.elem.length == 0;
}
//清空顺序表
public void clear(){
this.usedSize = 0;
/* for(int i = 0; i < this.usedSize; i++){
this.elem[i] = null;
}*/ //如果数组当中是引用类型
}
}
public class TestDemo{
public static void main(String[] args){
MyArrayList myArrayList = new MyArrayList();
//这里可以调用方法看看自己的代码是否有问题
}
}
下面我们分析一下部分方法的思路:
从以上代码来看,我们会发现一些有关顺序表的问题:
(1)顺序表中间 / 头部的插入删除,时间复杂度为 O(N)
(2)增容需要申请新空间,拷贝数据,释放旧空间。会有不小的消耗。
(3)增容一般是呈 2 倍的增长,势必会有一定的空间浪费。例如当前容量为 100 ,满了以后增容到 200 ,我们再继续插入了5 个数据,后面没有数据插入了,那么就浪费了 95 个数据空间。
但是这些问题都可以通过链表来解决
三、链表
1.概念及结构
(1)链表是一种 物理存储结构上非连续 存储结构,数据元素的 逻辑顺序 是通过链表中的 引用链接次序实现的。链表由若干个同一个结构类型的“节点”(“结点”)依次串接而成。
(2)链 表的结构非常多样,以下情况组合起来就有 8 种链表结构:
①单向、双向 ②带头、不带头 ③循环、非循环
单向带头循环、单向不带头循环、单向带头非循环、单向不带头非循环
双向带头循环、双向不带头循环、双向带头非循环、双向不带头非循环
但是我们现在重点掌握两种就好:
(i)无头单向非循环链表: 结构简单 ,一般不会单独用来存数据。实际中更多是作为 其他数据结构的子结构 ,如哈 希桶、图的邻接表等等
(ii) 无头双向链表:在 Java 的集合框架库中 LinkedList 底层实现就是无头双向循环链表
单链表:链表变量一般用head表示,用来存放链表首结点的地址,链表中的每个结点由数据部分(数据域)和下一个结点的地址部分组成,即每个结点都指向下一个结点,链表中的最后一个结点称为表尾,其下一个结点的地址部分的值为null
双链表:它的每个数据节点中都有两个引用,分别指向直接后继和直接前驱。所以,从双向链表中的任意一个结点开始,都可以很方便地访问它的前驱结点和后继结点
2.链表的实现
(1)无头单向非循环链表
//代表一个结点
class ListNode{
public int val;
public ListNode next;//存储的是节点的地址,所以它的类型是节点类型
public ListNode(){
this.val = val;
}
}
public class MyLinkedList{
//穷举法实现一个链表,其实实现一个链表最好用头插或尾插
public MyLinkedList 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);
listNode1.next=listNode2;
listNode2.next=listNode3;
listNode3.next=listNode4;
listNode4.next=listNode5;
//listNode5.next没有赋初值,相当于默认为null
this.head=listNode1;//head指向liatNode1所指向的对象
}
//打印链表 利用head,head去一个位置我们打印一个数,但是为了打印完后仍能找到头,我们要定义一个临时的头
public void display(){
ListNode cur = this.head;
while(cur != null){
System.put.println(cur.val+" ");
cur = cur.next;
}
System.put.println();
}
//查找单链表中是否包含关键字key
public boolean contains(int key){
ListNode cur = this.head;
whilr(cur != null){
if(cur.val == key){
return true;
}
cur = cur.next;
}
return false;
}
//得到单链表的长度
public int size(){
listNode cur = this.head;
int count = 0;
while(cur != null){
count++;
cur = cur.next;
}
return count;
}
}
public static void main(String[] args){
MyLinkedList myLinkedList = new MyLinkedList();
myLinkedList.createlist();
myLinkedList.display();
boolean flg = myLinkedList.contains(56);
System.out.println(flg);
System.out.println(myLinkedList.size());
}
}
相信这个穷举法已经帮我们很好的理解好链表的结构,现在我们头插实现一个链表
class ListNode{
public int val;
public ListNode next;//存储的是节点的地址,所以它的类型是节点类型
public ListNode(){
this.val = val;
}
}
public class MyLinkedList{
public MyLinkedList head;
public void display(){
ListNode cur = this.head;
while(cur != null){
System.put.println(cur.val+" ");
cur = cur.next;
}
System.put.println();
}
//头插法
public void addFirst(int data){
listNode node = new listNode(data);//data是一个数据,但是我们要插入的是一个结点,所以我们先new一个结点
/*if(this.head == null){
this.head = node;
}else{
node.next = this.head;
this.head = node;
}*/
//其实是不用分开写的
node.next = this.head;
this.head = node;
}
}
public static void main(String[] args) {
MyLinkedList myLinkedList = new MyLinkedList();
myLinkedList.addFirst(12);
myLinkedList.addFirst(123);
myLinkedList.display();
}
}
为什么不是12 23?因为它是头插法,23在12的前头插入的
我们还可以用尾插法实现一个链表
class ListNode{
public int val;
public ListNode next;//存储的是节点的地址,所以它的类型是节点类型
public ListNode(){
this.val = val;
}
}
public class MyLinkedList{
public MyLinkedList head;
public void display(){
ListNode cur = this.head;
while(cur != null){
System.put.println(cur.val+" ");
cur = cur.next;
}
System.put.println();
}
//尾插法
public void addLast(int data){
ListNode node = new listNode(data);
ListNode cur = this.head;
if(this.head == null){
this.head = node;
}else{
while(cur.next != null){
cur = cur.next;
}
cur.next = node;
}
}
public static void main(String[] args) {
MyLinkedList myLinkedList = new MyLinkedList();
myLinkedList.addLast(56);
myLinkedList.addLast(123);
myLinkedList.display();
}
}
这回的顺序是我们想要的了,因为23是在12的后头插的
接下来我们再来看看单链表的其他操作
class ListNode{
public int val;
public ListNode next;//存储的是节点的地址,所以它的类型是节点类型
public ListNode(){
this.val = val;
}
}
public class MyLinkedList{
public MyLinkedList head;
public listNode findIndex(int index){
listNode cur = this.head;
while(index - 1 != 0){
cur = cur.next;
index--;
}
return cur;
}
//任意位置插入,第一个数据节点为0号下标
public void addIndex(int index,int data){
if(index < 0 || index > size()){
return;
}
if(index == 0){
addFirst();
}
if(index == size()){
addLast();
}
listNode node = new listNode(data);
listNode cur = findIndex(index);
node.next = cur.next;
cur.next = node;
}
//删除第一次出现关键字为key的节点
public void remove(int key){
if(this.head == null){
System.out.println("单链表为空,不能删除");
return;
}
if(this.head.next == key){
this.head = this.head.next;
return;
}
ListNode cur = searchPerv(key);
if(cur == null){
System.out.println("没有你要找的节点");
return;
}
ListNode del = cur.next;
cur.next = del.next;
}
public ListNode searchPerv(int key){
ListNode cur = this.head;
while(cur.next != null){
if(cur.next.val == key){
return cur;
}
cur = cur.next;
}
return null;
}
//删除所有值为key的节点
public ListNode removeAllKey(int key){
if(this.head == null){
return null;
}
ListNode prev = this.head;
ListNode cur = this.head.next;
while(cur != null){
if(cur.val == key){
prev.next = cur.next;
cur = cur.next;
}else{
prev = cur;
cur =cur.next;
}
}
if(this.head.val == key){
this.head = this.head.naxt;
}
return this.head;
}
//清除链表
public void clear() {
//法一:(粗暴)
//this.head = null;
//法二(温柔)
while(this.head != null){
ListNode curNext = head.next;
this.head.next = null;
this.head = curNext;
}
}
}
public static void main(String[] args){
MyLinkedList myLinkedList = new MyLinkedList();
myLinkedList.createlist();
myLinkedList.display();
boolean flg = myLinkedList.contains(56);
System.out.println(flg);
System.out.println(myLinkedList.size());
}
}
下面我们分析一下部分方法的思路:
(2)无头双向链表实现
class ListNode{
public int val;
public ListNode prev;
public ListNode next;
public ListNode(int val){
this.val = val;
}
}
public class TestDemo {
public ListNode head;
public ListNode last;
//打印双向链表
public void display(){
ListNode cur = this.head;
while(cur != null){
System.out.print(cur.val+" ");
cur = cur.next;
}
System.out.println();
}
//得到双链表的长度
public int size(){
ListNode cur = this.head;
int count = 0;
while(cur != null){
count++;
cur = cur.next;
}
return count;
}
//查找是否包含关键字key是否在双链表当中
public boolean contains(int key){
ListNode cur = this.head;
while(cur != null){
if(cur.val == key){
return true;
}
cur = cur.next;
}
return false;
}
//头插法
public void addFirst(int data){
ListNode node = new ListNode(data);
if(this.head == null){
this.head = node;
this.last = node;
}else{
node.next = this.head;
this.head.prev = node;
this.head = node;
}
}
//尾插法
public void addLast(int data){
ListNode node = new ListNode(data);
if(this.head == null) {
this.head = node;
this.last = node;
}else{
this.last.next = node;
node.prev = this.last;
this.last = node;
}
}
//任意位置插入,第一个数据节点为0号下标
public void addIndex(int index,int data){
ListNode node = new ListNode(data);
if(index < 0 || index > size()){
return;
}
if(index == 0){
addFirst(data);
return;
}
if(index == size()){
addLast(data);
return;
}
ListNode cur = searchIndex(index);
node.next = cur.prev.next;
cur.prev.next = node;
node.prev = cur.prev;
cur.prev = node;
}
public ListNode searchIndex(int index){
ListNode cur = this.head;
while(index != 0){
cur = cur.next;
index--;
}
return cur;
}
//删除第一次出现关键字为key的节点
public void remove(int key){
ListNode cur = this.head;
while(cur != null){
if(cur.val == key){
//头结点
if(cur == this.head){
this.head = this.head.next;
//如果只有一个结点
if(this.head != null) {
this.head.prev = null;
}else{
last = null;
}
}else{
cur.prev.next = cur.next;
//尾结点
if(cur == this.last){
last = last.next;
}else{
cur.next.prev = cur.prev;//中间结点
}
}
return;
}
cur = cur.next;
}
}
//删除所有值为key的节点
public void removeAllKey(int key){
ListNode cur = this.head;
while(cur != null){
if(cur.val == key){
if(cur == head){
this.head = this.head.next;
if(head != null) {
this.head.prev = null;
}else{
last = null;
}
}else{
cur.prev.next = cur.next;
if(cur == last){
last = last.next;
}else{
cur.next.prev = cur.prev;
}
}
}
cur = cur.next;
}
}
public void clear(){
//法一:
/*this.head = null;
this.last = null;*/
//法二:
while(this.head != null){
ListNode curNext = this.head.next;
this.head.next = null;
this.head.prev = null;
this.head = curNext;
}
this.last = null;
}
public static void main(String[] args) {
TestDemo testDemo = new TestDemo();
//下面我们可以自己调用方法运行一下
}
}
分析部分方法的实现的思路:
四、顺序表和链表的区别与联系
1.顺序表
优势:空间连续、支持随机访问
劣势:①中间或前面部分的插入删除时间复杂度O(N) ②增容的代价比较大
2.链表
优势:①任意位置插入删除时间复杂度为O(1) ②没有增容问题,插入一个开辟一个空间
劣势:以节点为单位存储,不支持随机访问
总结:顺序表和链表的区别
XXX与XXX的不同的问题的作答,记住要从共同点出发,两者的共同点无非就是对数据的组织方式和描述方式不同
组织方式上:
(1)顺序表底层是一个数组,它是一个逻辑上和物理上都是连续的
(2)链表是一个由若干节点组成的一个数据结构,逻辑上是连续的,但是在物理上(内存上)不一定连续
操作上:
(1)顺序表适合查找相关的操作,因为可以使用下标,直接获取某个位置的元素
(2)链表适合于频繁的插入和删除的操作,此时不用像顺序表一样移动元素。链表的插入只需要修改指向
(3)顺序表满了之后要扩容,扩容之后,不一定全能放完,空间利用率不高