目录
1.数组(Array)
1.1 数组的特点
所谓数组,就是相同数据类型的元素按一定顺序排列的集合;数组的存储区间是连续的,占用内存比较大,故空间复杂的很大。但数组的二分查找时间复杂度小,都是O(1);数组的特点是:查询简单,增加和删除困难;
- 在内存中,数组是一块连续的区域
- 数组需要预留空间
- 在使用前需要提前申请所占内存的大小,如果提前不知道需要的空间大小时,预先申请就可能会浪费内存空间,即数组的空间利用率较低。注:数组的空间在编译阶段就需要进行确定,所以需要提前给出数组空间的大小(在运行阶段是不允许改变的)
- 在数组起始位置处,插入数据和删除数据效率低。
- 插入数据时,待插入位置的元素和他后面的所有元素都需要向后搬移
- 删除数据时,待删除位置后面的所有元素都需要向前搬移。
- 随机访问效率很高,时间复杂度可以达到O(1)
- 因为数组的内存是连续的,想要访问那个元素,直接从数组的首地址向后偏移就可以访问到了。
- 数组开辟的空间,在不够使用的时候需要进行扩容;扩容的话,就涉及到需要把旧数组中的所有元素向新数组中搬移。
- 数组的空间是从栈分配的。(栈:先进后出)
1.2 数组的优点:
- 随机访问性强,查找速度快,时间复杂度是0(1)
1.3 数组的缺点:
- 从头部删除、从头部插入的效率低,时间复杂度是o(n),因为需要相应的向前搬移和向后搬移。
- 空间利用率不高
- 内存空间要求高,必须要有足够的连续的内存空间。
- 数组的空间大小是固定的,不能进行动态扩展。
2.链表(ListNode)
2.1 链表的特点
所谓链表,链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针链接次序实现的。链表由一系列结点(链表中每一个元素称为结点)组成,结点可以在运行时动态生成。每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。 相比于线性表顺序结构,操作复杂。由于不必须按顺序存储,链表在插入的时候可以达到O(1)的复杂度,比另一种线性表顺序表快得多,但是查找一个节点或者访问特定编号的节点则需要O(n)的时间,而线性表和顺序表相应的时间复杂度分别是O(logn)和O(1)。
链表:链表存储区间离散,占用内存比较宽松,故空间复杂度很小,但时间复杂度很大,达O(N)。链表的特点是:查询相对于数组困难,增加和删除容易。
- 在内存中,元素的空间可以在任意地方,空间是分散的,不需要连续。
- 链表中的元素有两个属性,一个是元素的值,另一个是指针,此指针标记了下一个元素的地址。
- 每一个数据都会保存下一个数据的内存地址,通过该地址就可以找到下一个数据
- 查找数据时间效率低,时间复杂度是o(n)
- 因为链表的空间是分散的,所以不具有随机访问性,如果需要访问某个位置的数据,需要从第一个数开始找起,依次往后遍历,知道找到待查询的位置,故可能在查找某个元素时,时间复杂度是o(n)
- 空间不需要提前指定大小,是动态申请的,根据需求动态的申请和删除内存空间,扩展方便,故空间的利用率较高
- 任意位置插入元素和删除元素时间效率较高,时间复杂度是o(1)
- 链表的空间是从堆中分配的。(堆:先进先出,后进后出)
2.2链表的类型
2.2.1 单向链表
单向链表包含两个域,一个是信息域,一个是指针域。也就是单向链表的节点被分成两部分,一部分是保存或显示关于节点的信息,第二部分存储下一个节点的地址,而最后一个节点则指向一个空值。
2.2.2 双向链表
从上图可以很清晰的看出,每个节点有2个链接,一个是指向前一个节点(当此链接为第一个链接时,指向的是空值或空列表),另一个则指向后一个节点(当此链接为最后一个链接时,指向的是空值或空列表)。意思就是说双向链表有2个指针,一个是指向前一个节点的指针,另一个则指向后一个节点的指针。
2.2.3 循环链表
循环链表就是首节点和末节点被连接在一起。循环链表中第一个节点之前就是最后一个节点,反之亦然。
2.3 链表的优点
- 任意位置插入元素和删除元素的速度快,时间复杂度是o(1)
- 内存利用率高,不会浪费内存
- 链表的空间大小不固定,可以动态拓展。
2.4 链表的缺点
- 随机访问效率低,时间复杂度是o(1)
3.数组和链表的区别
数组和链表的区别主要表现在以下几个方面
比较项 | 数组 | 链表 |
---|---|---|
逻辑结构 | (1)数组在内存中连续; (2)使用数组之前,必须事先固定数组长度,不支持动态改变数组大小;(3) 数组元素增加时,有可能会数组越界;(4) 数组元素减少时,会造成内存浪费;(5)数组增删时需要移动其它元素 | (1)链表采用动态内存分配的方式,在内存中不连续 (2)支持动态增加或者删除元素 (3)需要时可以使用malloc或者new来申请内存,不用时使用free或者delete来释放内存 |
内存结构 | 数组从栈上分配内存,使用方便,但是自由度小 | 链表从堆上分配内存,自由度大,但是要注意内存泄漏 |
访问效率 | 数组在内存中顺序存储,可通过下标访问,访问效率高 | 链表访问效率低,如果想要访问某个元素,需要从头遍历 |
越界问题 | 数组的大小是固定的,所以存在访问越界的风险 | 只要可以申请得到链表空间,链表就无越界风险 |
4.数组和链表的使用场景
数组(顺序表)和链表各有短长,在实际应用中,应该具体问题具体分析,通常有以下方面的考虑
比较项 | 数组使用场景 | 链表使用场景 |
---|---|---|
空间 | 数组的存储空间是栈上分配的,存储密度大,当要求存储的大小变化不大时,且可以事先确定大小,宜采用数组存储数据 | 链表的存储空间是堆上动态申请的,当要求存储的长度变化较大时,且事先无法估量数据规模,宜采用链表存储 |
时间 | 数组访问效率高。当线性表的操作主要是进行查找,很少插入和删除时,宜采用数组结构 | 链表插入、删除效率高,当线性表要求频繁插入和删除时,宜采用链表结构 |
5.实现数组的增删改查(ArrayList)
5.1 自定义数组类
package Test;
public class MyArrayList {
private Object[] array;
/*添加元素add方法*/
public void add(Object ob){
if(array==null){
array=new Object[1];
array[0]=ob;
}else{
Object[] temp=new Object[array.length+1];
/*循环赋值*/
for (int i = 0; i < array.length; i++) {
temp[i]=array[i];
}
temp[array.length]=ob;
array=temp;
}
}
/*修改元素upd方法*/
public boolean set(int index,Object ob){
if(index<0||index>array.length){
return false;
}
array[index]=ob;
return true;
}
/*删除元素del方法*/
public boolean delete(int index){
Object[] temp=new Object[array.length-1];
if(index<0||index>temp.length){
return false;
}
/*删除对应下标的元素*/
/*循环赋值*/
for (int i = 0; i < temp.length; i++) {
if(i>=index){
temp[i]=array[i+1];
}else{
temp[i]=array[i];
}
}
array=temp;
return true;
}
/*查询所有*/
public void getall(){
for (Object o : array) {
System.out.println(o);
}
}
}
5.2 数组测试类
package Test;
public class test {
public static void main(String[] args) {
MyArrayList array=new MyArrayList();
array.add(1);
array.add(2);
array.add(3);
System.out.println("数组测试:正序输出:");
array.getall();
System.out.println("数组测试:新增:");
array.add(4);
array.getall();
System.out.println("数组测试:修改:");
array.set(2,"我被修改了!");
array.getall();
System.out.println("数组测试:删除:");
array.delete(2);
array.getall();
}
}
5.3 数组测试类结果
数组测试:正序输出:
1
2
3
数组测试:新增:
1
2
3
4
数组测试:修改:
1
2
我被修改了!
4
数组测试:删除:
1
2
4
进程已结束,退出代码为 0
6.实现链表的增删改查(LinkedList)
6.1 自定义单链表类
package LinkNode;
public class Node {
public Object data; //节点的内容
public Node next; //下一个节点
public Node(Object data) {
this.data = data;
}
}
package LinkNode;
public class MyLinkedList {
//根节点 找到链表的唯一途径
private Node root;
/*
* 往尾新增节点
* */
public void add(Object data){
/*新增进来的参数打包陈节点*/
Node node=new Node(data);
/*判断头节点是否为空如果是则新增头节点*/
if(root==null){
root=node;
}else{
Node temp=root; //用临时节点获得节点的头节点
while (temp.next!=null){ //循环判断下一个节点是否为空
temp=temp.next; //下移一个节点,既是赋值也是移动
}
temp.next=node; //给最后一个节点赋值
}
}
/*
* 往头新增节点
* */
public void addFirst(Object data){
/*新增进来的参数打包陈节点*/
Node node=new Node(data);
/*判断头节点是否为空如果是则新增头节点*/
if(root==null){
root=node;
}else{
node.next=this.root; //元素指向 原来头部的元素
this.root=node; //然后再吧要添加的元素做为头部
}
}
/*
*按照坐标进行插入
* */
private Node search(int index){
int count=0; //定义临时变量记录节点总长度
Node cur=this.root; //获得当前节点的头节点
for (int i = 0; i <index-1 ; i++) { //根据下标找到要插入节点的位置
cur=cur.next; //下移一个节点
count++;
}
return cur; //返回对应下标位置的节点
}
/*
* 验证下标是否正确
* */
private void checkIndex(int index){
if (index<0||index>getLength()){
throw new UnsupportedOperationException("位置不合法");
}
}
/*
* 按指定位置插入节点
* */
public void addIndex(int index, Object data) {
//如果输入的下标为0则为在头部插入
if (index==0){
addFirst(data);
//如果输入的下标超过节点下标的长度默认添加到最后一个
}else if(index>getLength()){
add(data);
//如果输入的下标不是0也不是最后一个那么就添加到对应的位置
}else{
//新增进来的参数打包成节点
Node node=new Node(data);
Node cur=search(index); //获得对应下标的节点
node.next=cur.next; //填充增加节点后的节点
cur.next=node; //插入对应节点
}
}
/*
* 获得当前节点的总长度
* */
public int getLength(){
int num=0; //创建临时计数器
Node temp=root; //创建临时变量获得节点的头
while (temp!=null) { //循环临时节点
temp=temp.next; //下移一个节点,既是赋值也是移动
num++;
}
return num; //返回当前节点总长度
}
/*
* 删除节点
* */
public void delete(int index){
//判断当前链表是否为空
if (root.next==null){ //空链表
System.out.println("链表为空,无法删除!");
return;
}
/*删除头节点需要单独判断*/
if(index==0){
root=root.next;
}else{
int num=0; //创建临时计数器
boolean flag=false; //标志是否找到待删除的节点
Node temp=root; //创建临时变量获得节点的头
while (temp!=null){ //循环临时节点
if(num==index-1){ //找到删除节点位置的上一个节点
temp.next=temp.next.next; //将当前节点替换为下一个节点
flag=true; //找到了
break;
}
temp=temp.next; //下移一个节点,既是赋值也是移动
num++; //临时节点++
}
if (flag){
}else{
System.out.println("没有找到对应的下标!");
}
}
}
/*
* 修改节点
* */
public void update(int index,Object date){
int num=0; //创建临时计数器
Node temp=root; //创建临时变量获得节点的头
while (temp!=null){ //循环临时节点
if(num==index){ //当下标与输入的下标一致时
temp.data=date; //替换当前节点的内容
}
temp=temp.next; //下移一个节点,既是赋值也是移动
num++; //临时节点++
}
}
/*
* 查询指定下标的节点内容
* */
public void getByindex(int index){
int num=0; //创建临时计数器
Node temp=root; //创建临时变量获得节点的头
while (temp!=null){ //循环临时节点
if(num==index){ //当下标与输入的下标一致时
System.out.println(temp.data); //输出该节点的内容
}
temp=temp.next; //循环赋值and循环节点
num++; //临时节点++
}
if(index<0){
System.out.println("节点下标不能小于0!");
}else if(index>num){
System.out.println("节点下标不能大于总长度!");
}
}
/*查询全部链表*/
public void getall(){
Node temp=root; //借用头节点用于循环整个链表
while (temp.next!=null){
System.out.println(temp.data);
temp=temp.next; //下移一个节点,既是赋值也是移动
}
/*最后一个节点一定会被遗漏,所以这里手动输出一下*/
System.out.println(temp.data);
}
}
6.2 自定义单链表测试类
package LinkNode;
public class NodeTest {
public static void main(String[] args) {
MyLinkedList linkedlist=new MyLinkedList();
linkedlist.add(11);
linkedlist.add(22);
linkedlist.add(33);
System.out.println("单链表测试:正序输出:");
linkedlist.getall();
System.out.println("单链表测试:增加:");
linkedlist.add(44);
linkedlist.getall();
System.out.println("单链表测试:头增:");
linkedlist.addFirst("现在我是链头了!");
linkedlist.getall();
System.out.println("单链表测试:按指定位置新增:");
linkedlist.addIndex(2,"想去哪就去哪!");
linkedlist.getall();
System.out.println("单链表测试:修改:");
linkedlist.update(2,"2号节点被修改了!");
linkedlist.getall();
System.out.println("单链表测试:删除:");
linkedlist.delete(5);
linkedlist.getall();
}
}
6.3 单链表测试类结果
单链表测试:正序输出:
11
22
33
单链表测试:增加:
11
22
33
44
单链表测试:头增:
现在我是链头了!
11
22
33
44
单链表测试:按指定位置新增:
现在我是链头了!
11
想去哪就去哪!
22
33
44
单链表测试:修改:
现在我是链头了!
11
2号节点被修改了!
22
33
44
单链表测试:删除:
现在我是链头了!
11
2号节点被修改了!
22
33
进程已结束,退出代码为 0
6.4 自定义双链表类
package DoubleLinkNode;
public class Node {
public Object data; //节点的内容
public Node next; //下一个节点
public Node prev; //上一个节点
public Node(Object data) {
this.data = data;
}
}
package DoubleLinkNode;
/**
* 单向链表与双向链表的区别
* 1.单向链表,查找的方向只能是一个方向,而双向链表可以向前或者向后查找
* 2.单向链表不能自我删除,需要靠辅助节点,而双向链表,则可以自我删除。
*/
public class MyLinkedList {
//根节点 找到链表的唯一途径
private Node root;
//未节点
private Node last;
/*
* 添加一个节点到双向列表的最后
* */
public void add(Object data){
/*新增进来的参数打包陈节点*/
Node node=new Node(data);
/*判断头节点是否为空如果是则新增头节点*/
if(root==null){
root=node;
}else{
Node temp=root; //用临时节点获得节点的头节点
while (temp.next!=null){ //循环判断下一个节点是否为空
temp=temp.next; //下移一个节点,既是赋值也是移动
}
temp.next=node; //给最后一个节点赋值
node.prev=temp; //尾节点指向前一个节点,形成一个双向链表
}
}
/*
* 往头新增节点
* */
public void addFirst(Object data){
/*新增进来的参数打包陈节点*/
Node node=new Node(data);
/*判断头节点是否为空如果是则新增头节点*/
if(root==null){
root=node;
}else{
node.next=this.root; //元素指向 原来头部的元素
this.root=node; //然后再吧要添加的元素做为头部
}
}
/*
*按照坐标进行插入
* */
private Node search(int index){
int count=0; //定义临时变量记录节点总长度
Node cur=this.root; //获得当前节点的头节点
for (int i = 0; i <index-1 ; i++) { //根据下标找到要插入节点的位置
cur=cur.next; //下移一个节点
count++;
}
return cur; //返回对应下标位置的节点
}
/*
* 验证下标是否正确
* */
private void checkIndex(int index){
if (index<0||index>getLength()){
throw new UnsupportedOperationException("位置不合法");
}
}
/*
* 按指定位置插入节点
* */
public void addIndex(int index, Object data) {
//如果输入的下标为0则为在头部插入
if (index==0){
addFirst(data);
//如果输入的下标超过节点下标的长度默认添加到最后一个
}else if(index>getLength()){
add(data);
//如果输入的下标不是0也不是最后一个那么就添加到对应的位置
}else{
//新增进来的参数打包成节点
Node node=new Node(data);
Node cur=search(index); //获得对应下标的节点
node.next=cur.next; //填充增加节点后的节点
cur.next=node; //插入对应节点
}
}
/*
* 获得当前节点的总长度
* */
public int getLength(){
int num=0; //创建临时计数器
Node temp=root; //创建临时变量获得节点的头
while (temp!=null) { //循环临时节点
temp=temp.next; //下移一个节点,既是赋值也是移动
num++;
}
return num; //返回当前节点总长度
}
/*
* 删除节点
* 说明
* 1 对于双向链表,我们可以直接找到要删除的这个节点
* 2 找到后,自我删除即可
* */
public void delete(int index){
//判断当前链表是否为空
if (root.next==null){ //空链表
System.out.println("链表为空,无法删除!");
return;
}
/*删除头节点需要单独判断*/
if(index==0){
root=root.next;
}else{
boolean flag=false; //标志是否找到待删除的节点
int num=0; //创建临时计数器
Node temp=root; //创建临时变量获得节点的头
while (temp!=null){ //循环临时节点
if(num==index){ //找到删除节点的位置
// temp.next=temp.next.next; //将当前节点替换为下一个节点 单链表
temp.prev.next=temp.next;
//如果是最后一个节点,就不需要执行下面这句话,否则出现空指针异常
if(temp.next!=null){
temp.next.prev=temp.prev;
}
flag=true; //找到了
break;
}
temp=temp.next; //下移一个节点,既是赋值也是移动
num++; //临时节点++
}
if (flag){
}else{
System.out.println("没有找到对应的下标!");
}
}
}
/*
* 双链表修改节点与单链表一致
* */
public void update(int index,Object date){
int num=0; //创建临时计数器
Node temp=root; //创建临时变量获得节点的头
while (temp!=null){ //循环临时节点
if(num==index){ //当下标与输入的下标一致时
temp.data=date; //替换当前节点的内容
}
temp=temp.next; //下移一个节点,既是赋值也是移动
num++; //临时节点++
}
}
/*
* 查询指定下标的节点内容
* */
public void getByindex(int index){
int num=0; //创建临时计数器
Node temp=root; //创建临时变量获得节点的头
while (temp!=null){ //循环临时节点
if(num==index){ //当下标与输入的下标一致时
System.out.println(temp.data); //输出该节点的内容
}
temp=temp.next; //循环赋值and循环节点
num++; //临时节点++
}
if(index<0){
System.out.println("节点下标不能小于0!");
}else if(index>num){
System.out.println("节点下标不能大于总长度!");
}
}
/*查询链表所有节点*/
public void AscList(){
Node temp=root; //借用头节点用于循环整个链表
while (temp.next!=null){
System.out.println(temp.data);
temp=temp.next; //下移一个节点,既是赋值也是移动
}
/*最后一个节点一定会被遗漏,所以这里手动输出一下*/
System.out.println(temp.data);
}
/*倒序输出*/
public void DescList(){
Node temp=root; //借用头节点用于循环整个链表
while (temp.next!=null){
temp=temp.next; //下移一个节点,既是赋值也是移动
}
while (temp.prev!=null){
System.out.println(temp.data);
temp=temp.prev; //上移一个节点,既是赋值也是移动
}
/*最后一个节点一定会被遗漏,所以这里手动输出一下*/
System.out.println(temp.data);
}
}
6.5 双链表测试类
package DoubleLinkNode;
public class NodeTest {
public static void main(String[] args) {
MyLinkedList linkedlist=new MyLinkedList();
linkedlist.add(11);
linkedlist.add(22);
linkedlist.add(33);
System.out.println("双向链表测试:正序输出:");
linkedlist.AscList();
System.out.println("双向链表测试:倒序输出:");
linkedlist.DescList();
System.out.println("双向链表测试:新增:");
linkedlist.add("44");
linkedlist.AscList();
System.out.println("双向链表测试:按指定位置新增:");
linkedlist.addIndex(2,"我是2号节点!");
linkedlist.AscList();
System.out.println("双向链表测试:头增:");
linkedlist.addFirst("现在我是链头了哦!");
linkedlist.AscList();
System.out.println("双向链表测试:修改:");
linkedlist.update(2,"2号节点被修改了!");
linkedlist.AscList();
System.out.println("双向链表测试:删除:");
linkedlist.delete(2);
linkedlist.AscList();
}
}
6.6 双链表测试类结果
双向链表测试:正序输出:
11
22
33
双向链表测试:倒序输出:
33
22
11
双向链表测试:新增:
11
22
33
44
双向链表测试:按指定位置新增:
11
22
我是2号节点!
33
44
双向链表测试:头增:
现在我是链头了哦!
11
22
我是2号节点!
33
44
双向链表测试:修改:
现在我是链头了哦!
11
2号节点被修改了!
我是2号节点!
33
44
双向链表测试:删除:
现在我是链头了哦!
11
我是2号节点!
33
44
进程已结束,退出代码为 0