✅作者简介:大家好,我是Philosophy7?让我们一起共同进步吧!🏆
📃个人主页:Philosophy7的csdn博客
🔥系列专栏:数据结构和算法
👑哲学语录: 承认自己的无知,乃是开启智慧的大门
💖如果觉得博主的文章还不错的话,请点赞👍+收藏⭐️+留言📝支持一下博>主哦🤞
文章目录
- 数据结构基础
-
- 🍌回忆什么是数组?
- 数组的基本操作
-
- 🍌什么是链表
- 🍌单向链表的基本操作
-
- 节点类
- 单向链表
- 约瑟夫环问题
- 判断两个单链表是否相交?若相交,求出交点。(2020年腾讯一面原题)
数据结构基础
🍌回忆什么是数组?
首先数组的英文单词是:array,是有限个相同类型的数据组成的有序集合,数组中的每一个变量称为元素。
数组是最简单、最常用
的数据结构,同时数组就属于线性结构
同时数组的特点是在内存中顺序存储
,因此逻辑上实现了顺序表
。
而内存是由一个个连续的内存单元组成的,每一个内存都有它自己的地址,而数组中的元素的内存地址是连续。
数组的基本操作
数组的基本操作无异于CRUD,现在就带大家回忆一下数组的基本用法。
package com.philosophy7.array;
public class ArrayTest {
public static void main(String[] args) {
//1.创建数组
//int[] arr = new int[5]; //动态创建数组的方式
int[] arr = new int[]{5,3,1,7,9}; //静态
//1.读取数组中的元素
System.out.println(arr[3]); //访问数组中的第四个元素 索引从0开始
//2.修改(更新)元素
arr[3] = 0; //修改第四个元素
System.out.println( "更新之后的数据:" + arr[3]);
//3.插入数据
/*
数组中可能存在这种情况:
实际存储的元素 小于 数组的大小
这里介绍数组常见的插入操作:
1.尾插
2.从中间插入
3.超范围插入
*/
int[] arr2 = new int[3];
arr2[0] = 1;
arr2[1] = 3;
MyArray array = new MyArray(3);
array.insert(0,5);
System.out.println(array.showSize());
array.insert(1,10);
array.insert(2,20);
System.out.println("===========");
array.display();
}
}
class MyArray{
private int[] array;
private int size;
public MyArray(){
}
//初始化数组
public MyArray(int capacity){
this.array = new int[capacity];
size = 0;
}
/**
* 数组插入元素
* @param index 插入的位置
* @param element 插入的元素
*/
public void insert(int index,int element){
//判断索引是否越界
if (index < 0 || index > size){
System.out.println("超出范围~!!");
return;
}
//从右向左循环 将元素逐个向右挪一位
//相当于尾插
for (int i = size -1; i >= index; i--){
array[i+1] = array[i];
}
//腾出的位置放入新元素
array[index] = element;
size++;
}
/**
* 遍历输出数组
*/
public void display(){
for (int i = 0; i < size; i++){
System.out.println(array[i]);
}
}
public int showSize(){
return size; //返回数组的大小
}
}
对上述代码进行解释说明:
成员变量size是数组实际元素的数量。如果插入元素在数组尾部,传入的下标参数 == size;如果插入元素在中间或者是在头部,则抛出异常。
当数组中的容量已经满了,这个时候仍需要插入元素,就需要涉及到扩容了
package com.philosophy7.array;
public class ArrayTest02 {
public static void main(String[] args) {
MyArray2 myArray2 = new MyArray2(3);
myArray2.insert(0,2);
myArray2.insert(1,4);
myArray2.insert(2,5);
myArray2.insert(3,6);
myArray2.delete(3);
myArray2.display();
}
}
class MyArray2{
private int[] array; //数组
private int size; //标识元素个数
//初始化数组
MyArray2(int capacity){
array = new int[capacity];
size = 0;
}
//插入数据
public void insert(int index,int element){
//1.首先判断是否索引越界
if (index < 0 || index > size){
System.out.println("超出数组实际范围存储");
return;
}
//2.扩容
if (size >= array.length){
resize();
}
for (int i = size -1 ; i >= index; i--){
array[i+1] = array[i];
}
}
/**
* arraycopy参数详解:
* src 源数组。
* * @param srcPos 在源数组中的起始位置。
* * @param dest 目标数组。
* * @param destPos 目标数据中的起始位置。
* * @param length 要复制的数组元素的数量。
*/
public void resize(){
int[] newArray = new int[array.length * 2];
//将旧数组复杂到新数组 完成扩容
System.arraycopy(array,0,newArray,0,array.length);
array = newArray;
}
//输出数组
public void display(){
for(int i = 0; i < size; i++){
System.out.println(array[i]);
}
}
public int delete(int index){
if (index < 0 || index >= size){
throw new IndexOutOfBoundsException("索引越界");
}
int deletedElement = array[index];
//元素逐个向左挪一位
for (int i = index; i < size-1; i++){
array[i] = array[i+1];
}
size--;
return deletedElement;
}
}
小结:
通过上述,我们对数组进行了增、删、改、查操作,现在我们来说一下数组的插入和删除操作的时间复杂度时间分别是多少
首先插入操作:
数组扩容的时间复杂度是O(n),插入并移动的时间复杂度也是O(n),综述数组的插入操作时间复杂度是O(n)
删除操作:
这里只涉及到了元素的移动,所以时间复杂度也是O(n)
🍌什么是链表
我们必须首先知道数据存储一般分为线性存储和链式存储两种。线性存储是一种顺序的存储的方式,就入刚才我们上述介绍的数组一样。
现在我们正式开始介绍链表:
我们首先来看一下单向链表的结构
链表与数组不同,数组是顺序存储的,而链表是一种在物理上非连续、非顺序的数据结构,由若干个节点(node)组成。
单向链表中的每个节点就包含两部分,一部分是存放数据的变量data,还有一部分是指向另一个节点的指针next
🍌单向链表的基本操作
基本操作也是一样的 无异于增、删、改、查
节点类
class LinkedNode {
private Object data; //数据
private LinkedNode next; //指针
public LinkedNode(Object data) {
this.data = data;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public LinkedNode getNext() {
return next;
}
public void setNext(LinkedNode next) {
this.next = next;
}
}
单向链表
增加节点就有:头插法、尾插法、中间插入
public class SingleLinkedList {
private LinkedNode head; //头节点
/**
* 头插法
* 将数据插入到链表的头部
* @param data
*/
public void addHeader(Object data){
//创建节点
LinkedNode temp = new LinkedNode(data);
if (head == null){ //头部为空 == 链表是为空的
head = temp; //传进来的该节点就为头节点
return;
}
//链表不为空 将新的节点放到链表的头部
temp.setNext(head); // 指针指向头节点
this.head = temp; //将头节点更换为temp
}
/**
* 尾插法:
* 将节点放于链表的尾部
* 尾部的指针指向Null
* @param data
*/
public void addTail(Object data){
//创建一个节点
LinkedNode temp = new LinkedNode(data);
if (head == null){
head = temp;
return;
}
//非空 找到最后一个节点
LinkedNode cur = head;
//从头节点开始往后面找 直到找到最后一个节点
//最后一个节点的指针 指向 null
while (cur.getNext() != null){
cur = cur.getNext();
}
cur.setNext(temp); //将找到最后一个节点的指针指向与我们要插入到尾部链表中去
}
//获取链表的长度
public int size(){
int size = 0;
for (LinkedNode node = this.head; node != null; node = node.getNext()){
size++;
}
return size;
}
//获取index位置节点
public LinkedNode getIndex(int index){
LinkedNode cur = this.head;
for (int i = 0; i < index; i++){
cur = cur.getNext();
}
return cur;
}
/**
* 将数据插入到指定的位置
* @param index
* @param data
*/
public void addIndex(int index,Object data){
//创建节点
LinkedNode node = new LinkedNode(data);
//校验传进的index是否合法
int len = size();
if (index < 0 || index > len){
System.out.println("不合法的操作");
return; //结束操作
}
//头插
if (index == 0){
addHeader(data);
}
//尾插
if (index == len){
addTail(data);
}
//插入到指定的位置 需要去找到插入位置的上一个节点
LinkedNode prev = getIndex(index - 1); //找到上一个节点
node.setNext(prev.getNext()); //设置新插入节点信息的指针指向于 前一个指针原指向的下一个指针
prev.setNext(node); //前一个节点指针 指向于 新插入的节点信息
}
//遍历节点
public void printInfo(){
System.out.print("[");
for (LinkedNode node = this.head; node != null; node = node.getNext()){
System.out.print(node.getData());
if (node.getNext() != null){
System.out.print(",");
}
}
System.out.println("]");
}
//删除节点
public void remove(int index){
//找到删除节点的前一个节点
if (head == null){
System.out.println("链表中没有节点可以删除");
return;
}
//删除的节点是否为头节点
LinkedNode removeNode = null;
if (index == 0){
removeNode = head;
head = head.getNext();
}
//删除中间节点
LinkedNode prev = getIndex(index - 1);
prev.setNext(prev.getNext().getNext());
}
public LinkedNode get(int index){
if (index < 0 || index >= size()){
System.out.println("您要查找的节点信息不存在");
return null;
}
LinkedNode temp = head;
for (int i = 0; i < index;i++){
temp = temp.getNext();
}
return temp;
}
}
其他图可以参考这些
现在我们来对比一下数组和链表
查找 | 更新 | 插入 | 删除 | |
---|---|---|---|---|
数组 | O(1) | O(1) | O(n) | O(n) |
链表 | O(n) | O(n) | O(1) | O(1) |
通过该表信息就可以发现,数组的优势在于能够快速定位元素,而对于读操作多、写操作少的场景来说,用数组更合适,之所以数组能够拥有快速查找的优势,就是基于数组当中是有索引(下标)。而链表当中是没有下标的
相反,链表的优势就在于能够快速的插入和删除操作,如果需要在尾部频繁插入、删除元素,链表的优势就体现出来了。
约瑟夫环问题
约瑟夫环(约瑟夫问题)是一个数学的应用问题:
已知n个人(以编号1,2,3…n分别表 示)围坐在一张圆桌周围。从编号为k的人开始报数,数到m的那个人出列;他的下一个人 又从1开始报数,数到m的那个人又出列;依此规律重复下去,直到圆桌周围的人全部出 列。
package com.philosophy7.linked;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
/**
* @author Philosophy7
*/
public class Yue {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
System.out.print("请输入总人数:");
int totalNum = scanner.nextInt();
System.out.print("请输入报数的大小:");
int cycleNum = scanner.nextInt();
yuesefu(totalNum, cycleNum);
scanner.close();
}
public static void yuesefu(int totalNum, int countNum) {
// 初始化人数
List<Integer> start = new ArrayList<Integer>();
for (int i = 1; i <= totalNum; i++) {
start.add(i);
}
// 从第K个开始计数
int k = 0;
while (start.size() > 0) {
k = k + countNum;
// 第m人的索引位置
k = k % (start.size()) - 1;
// 判断是否到队尾
if (k < 0) {
System.out.println(start.get(start.size() - 1));
start.remove(start.size() - 1);
k = 0;
} else {
System.out.println(start.get(k));
start.remove(k);
}
}
}
}
判断两个单链表是否相交?若相交,求出交点。(2020年腾讯一面原题)
思路:由于两个链表是不一样长度的,所以可以先求出两个链表的长度,然后用长度长的链表减去长度短的链表,得出一个长度k,然后先让链表长度长的链表先走k步,然后两个链表在一起走,只要链表的指针相等,那么这个时候指针指向的结点就是两个链表的交点了。