数据结构就是把数据元素按照一定的关系组织起来的集合,用来组织和存储数据;
顺序存储结构存在一定的弊端,就像生活中排队时也会有人插队也可能有人有特殊情况突然离开,这时候整个结构都处于变化中,此时就需要链式存储结构。
简单来说,就是根据一定的条件,对一些数据进行计算,得到需要的结果;
我们可以看到第二种解法明显更优,第一种解法需要执行100次加法运算,而第二种解法只需要完成三个运算逻辑即可完成需求;所以第二种它能够花费最少时间完成需求;
第一种解法,使用递归完成需求,fun1 方法被调用十次,并且在第一个方法未执行完毕时就会调用第二次方法,第二次执行未完毕时又会调用第三次执行,最多的时候需要在栈内存中同时开辟10块内存分别执行10个fun1方法。
第二种解法,使用for循环完成需求,fun2 方法只会执行一次,最终,只需要在栈内存开辟一块内存执行fun2 方法即可,很明显,第二种算法完成需求,同时占用的内存空间更小;
概述:
分析方法:
问题:
函数渐进增长:
首先我们要明确一个事情: 执行次数= 执行时间
算法1:
public static void main(String[] args){
int sum = 0; // 执行1 次
int n = 100; // 执行1 次
sum = (n+1)*n/2; // 执行1 次
System.out.println("sum="+sum);
}
算法2:
public static void main(String[] args){
int sum = 0; // 执行1次
int n = 100; // 执行1次
for(int i = 1 ; i <= n; i++){
sum += i; // 执行了n次
}
System.out.println("sum=" + sum);
}
算法3:
public static void mian(String[] args){
int sum = 0; //执行1次
int n = 100; // 执行1次
for(int i = 1 ; i <= n; i++){
for(int j = 1; j <=n ; j++){
sum+=i; // 执行n^2次
}
}
System.out.println("sum="+sum);
}
分析:
public static void main(String[] args){
int sum = 0;
int n = 100;
for (int i = 1; i <= n; i++){
sum += i;
}
System.out.println("sum="+sum);
}
上面这段代码,它的循环的时间复杂度为O(n),因为循环体中的代码需要执行n次;
平方阶
public static void main(String[] args){
int sum =0;n=100;
for(int i = 1; i< n; i++){
for(int j = 1; j<= n ; j++){
sum+= i;
}
}
System.out.println(sum);
}
上面这段代码,n=100,也就是说,外层每执行一次,内存循坏就执行100次,那总共程序想要从这两个循环中出来,就需要执行100*100次,也就是n的平方次,所以这段代码的时间复杂度是O(n^2);
立方阶
public static void main(String[] args){
int x =0;n=100;
for(int i = 1; i <=n; i++){
for(int j =i; i<=n; j++){
for(int j = i; j<=n ; j++){
x++;
}
}
}
}
上面这段代码,n = 100,也就是说,外层每执行一次,中间循环就执行100次,中间循环每执行一次,最内层循环就要执行100次,那总共程序想要从这三个循环中出来,就需要执行100100100次,就是n的立方,所以这段代码的时间复杂度是O(n^3);
对数阶
public static void main(String[] args){
int i =1,n=100;
while(i<n){
i = i*2;
}
}
由于每次i2 之后,就距离n更近一步。当(i2)执行x次之后大于指定的n(这里n是100),则会退出循环;所以可以得到 x^2=n 的公式,即 x=log(2)n,所以这个循环的时间复杂度为O(logn); (这里忽略了底数2)
从图里可以看出来增长趋势是一样的,虽然结果有一定差异,但这不是我们所关注的重点;
常数阶
public static void mian(String[] args){
int n = 100;
int i = n + 2;
System.out.println(i);
}
上述代码,不管输入规模n是多少,都执行2次,根据大O推导法则,常数用1来替换,所以上述代码的时间复杂度为O(1).
描述 | 增长的数量级 | 说明 | 举例 |
---|---|---|---|
常数级别 | 1 | 普通语句 | 将两个数相加 |
对数级别 | logN | 二分策略 | 二分查找 |
线性级别 | N | 循环 | 找出最大元素 |
线性对数级别 | NlogN | 分治思想 | 归并排序 |
平方级别 | N^2 | 双层循环 | 检查所有元素对 |
立方级别 | N^3 | 三层循环 | 检查所有三元组 |
指数级别 | 2^N | 穷举查找 | 检查所有子集合 |
如果发现算法的时间复杂度为平方阶、立方阶或者更复杂的,那我们可以认为这种算法是不可取的,需要优化
;public static void main(String[] args){
int n = 100;
for(int i = 0; i< n; i++){
show(i);
}
}
private static void show(int i){
System.out.println(i);
}
在main方法中,有一个for循环,循环体调用了show 方法,由于show 方法内部只执行了一行代码,所以show 方法的时间复杂度为O(1),那main方法的时间复杂度就是O(n);
public static void main(String[] args){
int n = 100;
for(int i = 0; i< n; i++){
show(i);
}
}
private static void show(int i){
for(int j = 0; j< i; i++){
System.out.println(i);
}
}
在main方法中,有一个for循环,循环体调用了show 方法,由于show 方法内部也有一个for循环,所以show方法的时间复杂度为O(n),那main方法的时间复杂度为O(n^2);
public static void main(String[] args){
int n = 100;
show(n);
for(int i = 0; i< n; i++){
show(i);
}
for(int i = 0; i< n; i++){
System.out.println(j);
}
}
private static void show(int i){
for(int j = 0; j< i; i++){
System.out.println(i);
}
}
在show方法中,有一个for循环,所以show方法的时间复杂度为O(n),在main方法中,show(n)这行代码内部执行的次数为n,第一个for循环内调用了show方法,所以其执行次数为n2,第二个嵌套for循环内只执行了一行代码,所以其执行次数为n2,那么main方法总执行次数为: n+n2+n2 = 2n2+n,根据大O推导规则,去掉n保留最高阶项,并去掉最高阶项的常数影子2,所以最终main方法的时间复杂度为O(n2);
public int search(int num){
int[] arr = {11,10,8,9,7,22,23,0};
for(int i = 0; i< arr.length; i++){
if(num == arr[i]){
return i;
}
}
return -1;
}
最坏情况是一种保证,在应用中,这是一种最基本的报账,即使在最坏的情况下,也能够正常提供服务,所以除非特别指定,我们提到的运行时间都指的是最坏情况下的运行时间;
数据类型 | 内存占用字节数 |
---|---|
byte | 1 |
short | 2 |
int | 4 |
long | 8 |
float | 4 |
double | 8 |
boolean | 1 |
char | 2 |
public class A {
public int a = 1;
}
那么创建该对象总共需要20个字节,但由于不是以8为单位,会自动填充为24个字节;
public static int[] reverse1(int[] arr){
int n = arr.length; //申请4个字节
int temp; // 申请4个字节
for(int start = 0; end = n-1; start <=end; start ++,end--){
temp = arr[start];
arr[start] = arr[end];
arr[end] = temp;
}
return arr;
}
public static int[] reverse2(int[] arr){
int n = arr.length; // 申请4个字节
int[] temp = new int[n]; // 申请n*4个字节+数组自身头信息开销24个字节
for(int i = n -1; i >=0; i--){
temp[n-1-i] = arr[i];
}
return temp;
}
排序概述:
在java 的开发工具包jdk中,已经给我们提供了很多数据结构与算法的实现,比如List,Set,Map,Math等等,都是以API的方式提供,这种方式的好处在于一次编写,多处使用;
Comparable接口介绍:
// 学生类
public class Student implements Comparable<Student>{
private String username;
private int age;
// 省略getter、setter、toString()方法
@Override
public int compareTo(Student o){
return this.getAge()-o.getAge();
}
}
public class TestComparable{
public static void main(String[] args){
// 创建两个student对象,并调用getMax方法,完成测试
Student s1 = new Student();
s1.setUsername("张三");
s1.setAge(18);
Student s2 = new Student();
s2.setUsername("李四");
s2.setAge(20);
Comparable max = getMax(s1,s2);
System.out.println(max);
}
public static Comparable getMax(Comparable c1, Comparable c2){
int result = c1.compareTo(c2);
// 如果result < 0,则c1 比c2 小;
// 如果result > 0,则c1 比c2 大;
// 如果result == 0, 则c1 和 c2一样大;
if(result >= 0){
return c1;
}else{
return c2;
}
}
}
public class Bubble{
/*
* 对数组a中的元素进行排序
*/
public static void sort(Comparable[] a){
for(int i = a.length-1;i>0; i--){
// 比较索引j 和索引j+1处的值
if(greater(a[j],a[j+1])){
exch(a,j,j+1);
}
}
}
/*
* 比较v元素是否大于w元素
*/
public static boolean greater(Comparable v, Comparable w){
return v.compareTo(w) > 0;
}
/*
* 数组元素i和j交换位置
*/
public static void exch(Comparable[] a,int i , int j){
Comparable temp;
temp = a[i];
a[i]=a[j];
a[j]=temp;
}
}
public class BubbleTest{
Integer[] arr ={4,5,6,3,2,1};
Bubble.sort(arr);
System.out.println(Arrays.toString(arr));
}
概述:
代码实现:
public class Selection {
/*
* 对数组a中的元素进行排序
*/
public static void sort(Comparablep[] a){
for(int i = 0; i<= a.length-2; i++){
// 定义一个变量,记录最小元素所在索引,默认为参与选择排序的第一个元素所在的位置
for(int j = i+1; j<a.length; j++){
// 需要比较最小索引minIndex处的值和j索引处的值
if(greater(a[minIndex],a[j])){
minIndex = j;
}
}
// 交换最小元素所在索引minIndex 处的值和索引i处的值;
exch(a,i,minIndex);
}
/*
* 比较v元素是否大于w元素
*/
private static boolean greater(Comparable v,Comparable w){
return v.compareTo(w)>0;
}
/*
* 数组元素i和j交换位置
*/
public static void exch(Comparable[] a,int i,int j){
Comparable temp;
temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
}
测试类
public class SelectionTest {
// 原始数据
Integer[] a = {4,6,8,7,9,2,10,1};
Selection.sort(a);
System.out.println(Arrays.toString(a));
}
选择排序的时间复杂度分析:
概述:
排序原理:
代码实现
public class Insertion {
/*
* 对数组a中的元素进行排序
*/
public static void sort(Comparable[] a){
for(int i = 1; i < a.length; i++){
for(int j = i; j >= 0; j--){
// 比较索引j处的值和索引j-1处的值,如果索引j-1 处的值比索引j处的值大,则交换数据。如果不大,那么就找到合适的位置了,退出循环即可;
if(greater(a[j-1]),a[j]){
exch(a,j-1,j);
}else{
break;
}
}
}
}
/*
* 比较v元素是否大于w元素
*/
private static boolean greater(Comparable v, COmparable w){
return v.compareTo(w)>0;
}
/*
* 数组元素i和j交换位置
*/
private static void exch(Comparable[] a,int i, int j){
Comparable temp;
temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
测试
public calss IntertionTest {
Integer[] a={4,3,2,10,12,1,5,6};
Intertion.sort(a);
System.out.println(Arrays.toString(a));
}
插入排序的时间复杂度分析
概述:
排序原理:
int h = 1;
while(h < 数组的长度/2){
h = 2h +1;
}
// 循环结束后我们就可以确定h的最大值;
h 的减小规则为:
h = h/2;
它最大的优势在于不是挨着进行交换,而是可以在间隔很大的两个元素也可以完成交换,减少了交换的成本,从而提高了效率;
抛出问题:
代码实现:
public class Shell {
/**
* 对数组a中的元素进行排序
*/
public static void sort(Comparable[] a){
// 1. 根据数组a的长度,确定增长量h的初始值
int h = 1;
while(h < a.length/2){
h = 2*h+1;
}
// 2. 希尔排序
while(h >= 1){
// 排序
// 2.1 找到待插入的元素
for(int i = h; i < a.length;i++){
for(int j = i; j >= h; j-= h){
// 待插入的元素是a[j],比较a[j] 和 a[j-h]
if(greater(a[j-h],a[j])){
exch(a,j-h,j);
}else{
// 待插入元素已经找到了合适的位置,结束循环;
break;
}
}
}
// 减少h的值
h = h/2;
}
}
/*
* 比较v元素是否大于w元素
*/
private static boolean greater(Comparable v, COmparable w){
return v.compareTo(w)>0;
}
/*
* 数组元素i和j交换位置
*/
private static void exch(Comparable[] a,int i, int j){
Comparable temp;
temp = a[i];
a[i] = a[j];
a[j] = temp;
}
}
测试类:
public class ShellTest{
public static void main(String[] args){
Integer[] a = {9,1,2,5,7,4,8,6,3,5};
Shell.sort(a);
System.out.println(Arrays.otString(a)); // {1,2,3,4,5,6,7,8,9}
}
}
希尔排序的时间复杂度分析
同学们可以自己本地实测一下;具体代码就不贴了,前面文章认真看了相信可以自己写出来;
public void show() {
System.out.println("aaa");
show();
}
public class Test{
public static void main(String[] args) throws Exception{
int result = factorial(5);
System.out.println(result);
}
public static int factorial(int n){
if(n==1){
return 1;
}
}
}
由于没有递归最大次数上限,所以这段代码执行时一定会栈内存溢出;平时使用递归时,一定要注意;
概述:
需求:
归并排序原理:
归并代码:
public class Merge{
// 归并所需要的辅助数组
private static Comparable[] assist;
/*
* 比较v元素是否小于w元素
*/
private static boolean less(Comparable v, Comparable w){
return v.compareTo(w)<0;
}
/*
* 数组元素i和j交换位置
*/
private static void exch(Comparable[] a,int i, int j){
Comparable t = a[i];
a[i] = a[j];
a[j] = t;
}
/*
* 对数组a中的元素进行排序
*/
public static void sort(Comparable[] a){
// 1. 初始化辅助数组 assist;
assist = new Comparable[a.length];
// 2. 定义一个lo变量,和hi变量,分别记录数组中最小的索引和最大的索引;
int lo = 0;
int hi = a.length-1;
// 3. 调用sort 重载方法完成数组a中,从索引lo到索引hi的元素的排序;
sort(a,lo,hi);
}
/*
* 对数组a中从lo到hi的元素进行排序
*/
private static void sort(Comparable[] a,int lo, int hi){
// 做安全性校验
if(hi <= lo){
return;
}
// 对lo到hi之间的数据进行分为两个组
int mid = lo + (hi-lo)/2;
// 分别对每一组数据进行排序
sort(a,lo,mid);
sort(a,mid+1,hi);
// 再把两个组中的数据进行归并
merge(a,lo,mid,hi);
}
/*
* 对数组中,从lo到mid为一组,从mid+1到hi为一组,对这两组数据进行归并;
*/
private static void merge(Comparable[] a, int lo, int mid, int hi){
// 定义三个指针
int i = lo;
int p1 = lo;
int p2 = mid +1;
// 遍历,移动p1指针和p2指针,比较对应索引处的值,找出小的那个,放到辅助数据的对应索引处;
while(p1 < mid && p2 <= hi){
// 比较对应索引处的值
if(less(a[p1],a[p2])){
assist[i++] = a[p1++];
}else{
assist[i++] = a[p2++];
}
}
// 遍历,如果p1 的指针没有走完,那么顺序移动p1指针,把对应的元素放到辅助数组的对应索引处;
while(p1 <= mid){
assist[i++] = a[p1++];
}
// 遍历,如果p2的指针没有走完,那么顺序移动p2指针,把对应的元素放到辅助数组的对应索引处;
while(p2<=hi){
assist[i++] = a[p2++];
}
// 把辅助数组中的元素拷贝到原数组中
for(int index = lo; index <= hi; index++){
a[index] = assist[index];
}
}
}
代码逻辑图示:
测试类:
public static MergeTest{
public static void main(String[] args){
Integer[] a = {8,4,5,7,1,3,6,2};
Merge.sort(a);
System.out.println(Arrays.toString(a)); // {1,2,3,4,5,6,7,8}
}
}
归并排序时间复杂度分析:
归并排序的缺点:
归并排序与希尔排序性能测试:
概述:
需求:
排序原理:
代码演示:
public class Quick {
/*
* 比较v元素是否小于w元素
*/
private static boolean less(Comparable v, Comparable w){
return v.compareTo(w) <0;
}
/*
* 数组元素i和j交换位置
*/
private static void exch(Comparable[] a,int i, int j){
Comparable t = a[i];
a[i] = a[j];
a[j] = t;
}
/*
* 对数组内的元素进行排序
*/
public static void sort(Comparable[] a){
int lo = 0;
int hi = a.length -1;
sort(a,lo,hi);
}
/*
*对数组a中从索引lo到索引hi之间的元素进行排序
*/
private static void sort(Comparable[] a,int lo, int hi){
// 安全性校验
if(hi<= lo){
return;
}
// 需要对数组中lo索引到hi索引处的元素进行分组(左子组合右子组);
int partition = partition(a,lo,hi); // 返回的是分组的分界值所在的索引。
// 让左子组有序
sort(a,lo,partition-1);
// 让右子组有序
sort(a,partition+1,hi);
}
/*
* 对数组a中,从索引lo到索引hi之间的元素进行分组,并返回分组界限对应的索引;
*/
public static int partition(Comparable[] a, int lo, int hi){
// 确定分界值
Comparable key = a[lo];
// 定义两个指针,分别指向待切分元素的最小索引处和最大索引处的下一个位置
int left = lo;
int right = hi +1;
// 切分
while(true){
// 先从右往左扫描,移动right指针,找到一个比分界值小的元素,停止;
while(less(key,a[--right])){
if(right == lo){
break;
}
}
// 再从左往右扫描,移动left指针,找到一个比分界值大的元素,停止;
while(left(a[++left],key)){
if(left == hi){
break;
}
}
// 判断left >= right ,如果是,则证明元素扫描完毕,结束循环,如果不是,则交换元素即可;
if(left >= right){
break;
}else{
exch(a,left,right);
}
}
// 交换分界值
exch(a,lo,right);
return right;
}
}
测试类
public class QuickTest {
public static void main(String[] args){
Integer[] a= {6,1,2,7,9,3,4,5,8};
Quick.sort(a);
System.out.println(Arrays.toString(a)); // {1,2,3,4,5,6,7,8,9}
}
}
快速排序和归并排序的区别?
快速排序没有归并操作,快速排序按照分界值切分,并不一定是等分;
快速排序时间复杂度分析:
稳定性的定义:
稳定性的意义:
常见算法的稳定性:
如果把线性表用数学语言来定义,则可以表示为(a1,…ai-1,ai,ai+1,…an),ai-1领先于ai,ai领先于ai+1,称ai-1是ai的前驱元素,ai+1是ai的后继元素;
由此我们可以知道,数组是顺序表的实现,链表链式存储的实现,它们都属于线性表,符合线性表的规范;
概述:
代码演示:
public class SequenceList<T>{
// 存储元素的数组
private T[] eles;
// 记录当前顺序表中的元素个数
private int N;
// 构造方法
public SequenceList(int capacity){
// 初始化数组
this.elses = (T[])new Object[capacity];
this.N =0;
}
// 将一个线性表置为空表
public void clear(){
this.N = 0;
}
// 判断当前线性表是否为空表
public boolean isEmpty(){
return N == 0;
}
// 获取线性表的长度
public int length(){
return N;
}
// 获取指定位置的元素
public T get(int i){
return eles[i];
}
// 向线性表中添加元素t
public void insert(T t){
eles[N++]=t;
}
// 在i元素处插入元素t
public void insert(int i, T t){
// 先把i索引处的元素及其后面的元素依次向后移动一位
for(int index = N-1; index > i; index --){
eles[index] = eles[index -1];
}
// 再把t元素放到i索引处即可
eles[i] = t;
// 元素个数+1
N++;
}
// 删除指定位置i处的元素,并返回该元素
public T remove(int i){
// 记录索引i处的值
T current = eles[i];
// 索引i后面元素依次向前移动一位即可
for(int index = i; index < N-1; index ++){
eles[index] = eles[index + 1];
}
// 元素个数 -1
N --;
return current;
}
// 查找t元素第一次出现的位置
public int indexOf(T t){
for(int i = 0; i < N; i++){
if(eles[i].equals(t)){
return i;
}
}
return -1;
}
}
测试代码:
一般作为容器存储数据,都需要向外部提供遍历的方式,因此我们需要给顺序表提供遍历方式。
在java中,遍历集合的方式一般都是用的是foreach循环,如果想让我们的SequenceList也能支持foreach循环,则需要做如下操作:
代码演示:
public class SequenceList<T> implements Iterable<T>{
// 存储元素的数组
private T[] eles;
// 记录当前顺序表中的元素个数
private int N;
// 构造方法
public SequenceList(int capacity){
// 初始化数组
this.elses = (T[])new Object[capacity];
this.N =0;
}
// 将一个线性表置为空表
public void clear(){
this.N = 0;
}
// 判断当前线性表是否为空表
public boolean isEmpty(){
return N == 0;
}
// 获取线性表的长度
public int length(){
return N;
}
// 获取指定位置的元素
public T get(int i){
return eles[i];
}
// 向线性表中添加元素t
public void insert(T t){
eles[N++]=t;
}
// 在i元素处插入元素t
public void insert(int i, T t){
// 先把i索引处的元素及其后面的元素依次向后移动一位
for(int index = N-1; index > i; index --){
eles[index] = eles[index -1];
}
// 再把t元素放到i索引处即可
eles[i] = t;
N++;
}
// 删除指定位置i处的元素,并返回该元素
public T remove(int i){
// 记录索引i处的值
T current = eles[i];
// 索引i后面元素依次向前移动一位即可
for(int index = i; index < N-1; index ++){
eles[index] = eles[index + 1];
}
// 元素个数 -1
N --;
return current;
}
// 查找t元素第一次出现的位置
public int indexOf(T t){
for(int i = 0; i < N; i++){
if(eles[i].equals(t)){
return i;
}
}
return -1;
}
@Override
public Iterator<T> iterator(){
return new SIterator();
}
private class SIterator implements Iterator{
private int cusor;
public SIterator(){
this.cusor = 0;
}
@Override
public boolean hasNext(){
return cusor < N;
}
@Override
public Object next(){
return eles[cusor++];
}
}
}
引导语:
考虑容器的容量伸缩性,其实就是改变存储数据元素的数组的大小,那我们需要考虑什么时候需要改变数组的大小?
什么时候考虑改变数组的大小?
代码演示:
由于顺序表的低层由数组实现,数组的长度是固定的,所以在操作的过程中涉及到了容器扩容操作。这样会导致顺序表在使用过程中的时间复杂度不是线性的,在某些需要扩容的节点处,耗时会突增,尤其是元素越多,这个问题越明显;
为什么要使用链表?
链表概述:
代码演示:
public class Node<T>{
// 存储元素
public T item;
// 指向下一个结点
public Node next;
public Node(T item,Node next){
this.item = item;
this.next = next;
}
}
public static void main(String[] args) throws Exception{
// 构建结点
Node<Integer> first = new Node<Integer>(11,null);
Node<Integer> second = new Node<Integer>(13,null);
Node<Integer> third = new Node<Integer>(12,null);
Node<Integer> fourth = new Node<Integer>(8,null);
Node<Integer> fifth = new Node<Integer>(9,null);
// 生成链表
first.next = second;
second.next = third;
third.next = fourth;
fourth.next = fifth;
}
概述:
单向链表API设计
单向链表代码实现
public class LinkList<T> implements Iterable{
// 记录头结点
private Node head;
// 记录链表的长度
private int N;
// 结点类
private class Node{
// 存储数据
T item;
// 下一个结点
Node next;
public Node(T item, Node next){
this.item = item;
this.next = next;
}
}
public LinkList(){
// 初始化头结点
this.head = new Node(null,null);
// 初始化元素个数
this.N = 0;
}
// 清空链表
public void clear(){
head.next = null;
this.N =0;
}
// 获取链表的长度
public int length(){
return N;
}
// 判断链表是否为空
public boolean isEmpty(){
return N==0;
}
// 获取指定位置i处的元素
public T get(int i){
// 通过循环,从头结点开始往后找,依次找i次,就可以找到对应元素
Node n = head.next;
for(int index =0; index < i; index ++){
n = n.next;
}
return n.item;
}
// 向链表中添加元素
public void insert(T t){
// 找到当前最后一个结点
Node n = head;
while(n.next != null){
n = n.next;
}
// 创建新结点,保存元素t
Node newNode = new Node(t,null);
// 让当前最后一个结点指向新结点;
n.next = newNode;
// 元素的个数+1
N++;
}
// 向指定位置i处添加元素t
public void insert(int i, T t){
// 找到i位置前一个结点
Node pre = head;
for(int index = 0; index <= i -1; i++){
pre = pre.next;
}
// 找到i位置的结点
Node curr = pre.next;
// 创建新结点,并且新结点需要指向原来i位置的结点
Node newNode = new Node(t,curr);
// 原来i位置的前一个结点指向新结点即可
pre.next = newNode;
// 元素的个数+1
N++;
}
// 删除指定位置i处的元素,并返回被删除的元素
public T remove(int i){
// 找到i位置的前一个结点
Node pre = head;
for(int index =0; index <=i-1; i++){
pre = pre.next;
}
// 要找到i位置的节点
Node curr = pre.next;
// 找到i位置的下一个节点
Node nextNode = curr.next;
// 前一个结点指向下一个结点
pre.next = nextNode;
// 元素个数 -1
N--;
return curr.item;
}
// 查找元素i在链表中第一次出现的位置
public int indexOf(T t){
// 从头结点开始,依次找到每一个结点,取出item和t比较,如果相同就找到了;
Node n = head;
for(int i = 0; n.next != null; i++){
n = n.next;
if(n.item.equals(t)){
return i;
}
}
return -1;
}
@Override
public Iterator<T> iterator(){
return new LIterator();
}
private class LIterator implements Iterator{
private Node n;
public LIterator(){
this.n = head;
}
@Override
public boolean hasNext(){
return n.next != null;
}
@Override
public Object next(){
n = n.next;
return n.item;
}
}
}
测试类:
概述:
API的设计:
按照面向对象的思想,我们需要设计一个类,来描述结点这个事物,由于结点是属于链表的,所以我们把结点类作为链表类的一个内部类来实现;
代码演示:
public class TowWayLinkList<T> implements Iterable<T>{
// 首结点
private Node head;
// 最后一个结点
private Node last;
// 链表的长度
private int N;
// 结点类
private class Node{
public Node(T item, Node pre, Node next){
this.item = item;
this.pre = pre;
this.next = next;
}
// 存储数据
public T item;
// 指向上一个结点
public Node pre;
// 指向下一个结点
public Node next;
}
public TowWayLinkList(){
// 初始化头结点和尾结点
this.head = new Node(null,null,null);
this.last = null;
// 初始化元素个数
this.N = 0;
}
// 清空链表
public void clear(){
this.head.next = null;
this.head.pre = null;
this.head.item = null;
this.last = null;
this.N = 0;
}
// 获取链表长度
public int length(){
return N;
}
// 判断链表是否为空
public boolean isEmpty(){
return N ==0;
}
// 获取第一个元素
public T getFirst(){
if(isEmpty()){
return null;
}
return head.next.item;
}
// 获取最后一个元素
public T getList(){
if(isEmpty()){
return null;
}
return last.item;
}
// 插入元素i
public void insert(T t){
if(isEmpty()){
// 如果链表为空
// 创建新的结点
Node newNode = new Node(t,head,null);
// 让新结点成为尾结点
last = newNode;
// 让头结点指向尾结点
head.next = last;
}else{
// 如果链表不为空
Node oldLast = last;
// 创建新的结点
Node newNode = new Node(t,oldLast,null);
// 让当前的尾结点指向新结点
oldLast.next = newNode;
// 让新结点成为尾结点
last = newNode;
}
// 元素个数+1
N++;
}
// 向指定位置i处插入元素t
public void isnert(int i , T t){
// 找到i位置的前一个结点
Node pre = head;
for(int index =0; index < i; index ++){
pre = pre.next;
}
// 找到i位置的结点
Node curr = pre.next;
// 创建新结点
Node newNode = new Node(t,pre,curr);
// 让i位置的前一个结点的下一个结点变为新结点
pre.next = newNode;
// 让i位置的前一个结点变为新结点
curr.pre = newNode;
// 元素个数+1
N++;
}
// 获取指定位置i处的元素
public T get(int i){
Node n = head.next;
for(int i = 0; n.next != null; i++){
n = n.next;
if(n.next.equals(t)){
return i;
}
}
return -1;
}
// 找到元素t 在链表中第一次出现的位置
public int indexOf(T t){
Node n = head;
for(int i =0; n.next != null; i++){
n = n.next;
if(n.next.equals(t)){
return i;
}
}
return -1;
}
// 删除位置i处的元素并返回该元素
public T remove(int i){
// 找到i位置的前一个结点
Node pre = head;
for(int index = 0; index <i; index++){
pre = pre.next;
}
// 找到i位置的结点
Node curr = pre.next;
// 找到i位置的下一个结点
Node nextNode = curr.next;
// 让i位置的前一个结点的下一个结点变为i位置的下一个结点
pre.next = nextNode;
// 让i位置的下一个结点的上一个节点变为i位置的前一个结点
nextNode.pre = pre;
// 元素的个数 -1
N--;
return curr.item;
}
@Override
public Iterator<T> iterator(){
return new TIterator();
}
private class TIterator implements Iterator{
private Node n;
public TIterator(){
this.n = head;
}
@Override
public boolean hasNext(){
return n.next != null;
}
@Override
public Object next(){
n = next;
return n.item;
}
}
}
关于链表和数组的对比,我们不能够道听途说,真正地理解了它底层实现,相信每个同学都会有自己的判断;
单链表的反转,是面试中的一个高频题目。
需求:
反转api设计:
原理分析:
代码演示:【为方便展示关键点,此处只写反转部分代码,其他部分请找上一章节的代码】
// 用来反转整个链表
public void reverse(){
// 判断当前链表是否为空链表,如果是空链表则结束运行
if(isEmpty()){
return;
}
reverse(head.next);
}
// 反转指定的结点curr,并把反转后的结点返回
public Node reverse(Node curr){
if(curr.next == null){
head.next = curr;
return curr;
}
// 递归的反转当前结点curr的下一个结点;返回值就是链表翻转后,当前结点的上一个结点;
Node pre = reverse(curr.next);
// 让返回的结点的下一个结点变成当前结点curr;
pre.next = curr;
// 把当前结点的下一个结点变为null
curr.next = null;
return curr;
}
测试类:
本篇博客还没有结束,更多精华后续陆续更新哦!欢迎收藏点赞评论!