1.实现一个计数器
class Calculator{
private int num1;
private int num2;
public int getNum1() {
return num1;
}
public void setNum1(int num1) {
this.num1 = num1;
}
public int getNum2() {
return num2;
}
public void setNum2(int num2) {
this.num2 = num2;
}
public int add()
{
return num1+num2;
}
public int sub()
{
return num1-num2;
}
public double mul()
{
return num1*num2;
}
public double dev()
{
return num1/num2;
}
}
public class test {
public static void main(String[] args) {
Calculator calculator=new Calculator();
calculator.setNum1(10);
calculator.setNum2(5);
System.out.println(calculator.add());
System.out.println(calculator.sub());
}
}
2.交换两个数的值
public static void swap(Integer s1,Integer s2)
{
int temp=s1.num;
s1.num=s2.num;
s2.num=temp;
}
public static void main(String[] args) {
Integer s1=new Integer();
s1.num=10;
Integer s2=new Integer();
s2.num=20;
swap(s1,s2);
System.out.println(s1.num);
System.out.println(s2.num);
}
3.时间复杂度与空间复杂度:电脑的硬件设备是不同的,由于相同的代码在不同的硬件系统有不同的行为,不可以用时间戳来进行衡量
时间复杂度是衡量一个算法的运行速度,空间复杂度是衡量一个算法所需要的额外空间,
算法中基本操作的执行次数称之为算法的时间复杂度,因为一个算法的所花费的时间与其中语句的执行次数成正比,先找执行语句次数最多的,先找循环,这种大O渐进表示法去掉了那些对接过影响不大的项,简洁明了的表示出了执行次数,因为N逐渐变得越来越大,那些小数字都变成了0头,所以说没有必要要进行计算
我们再进行计算时间复杂度的时候
1)用常数1取代运行时间的所有加法常数
2)在修改后的运行次数函数中,只保留最高阶项
3)如果在高阶项存在况且不是1,那么就去除与这个项相乘的常数,得到的结果就是O阶
最坏情况:任意输入规模的最大运行次数
最好情况:任意输入规模的最小执行次数
平均情况:任意输入规模的平均执行次数,在这里面我们要根据代码的情况给出平均时间复杂度
比如说现在咱们在一个长度为N的数组中查找一个数据X
最好情况下:1次找到
最坏情况下:N次找到
平均情况:N/2次找到
例1:
注意:我们根据第三条来说,如果算出的时间复杂度是3N^2;那么最终的时间复杂度就是N^2
void func1(int N){
int count = 0;
for (int i = 0; i < N ; i++) {
for (int j = 0; j < N ; j++){
count++;
}
}
for (int k = 0; k < 2 * N ; k++)
{
count++;
}
int M = 10;
while ((M--) > 0)
{count++;}
System.out.println(count);
}
他的时间复杂度为N^2+2N+10
只保留最高阶项,那么就把2N去掉,常数全部变成1,那么最终只剩下N^2+1;最终也就是N^2
// 计算func3的时间复杂度
void func3(int N, int M)
{int count = 0;
for (int k = 0; k < M; k++)
{count++;
}
for (int k = 0; k < N ; k++)
{count++;
}
System.out.println(count);
}
O(M+N)
for (int k = 0; k < 2 * N ; k++)
{count++;}
int M = 10;
while ((M--) > 0)
{count++;}
时间复杂度是O(N)-->2N+10--->O(N)
void func4(int N){
int count=0;
for(int i=0;i<1000;i++)
count++;
System.out.println(count);
}
所有常数均变成1000,那么时间复杂度就是O(1)
冒泡排序:N*(N-1):随着问题规模的增大,用到的变量都是同一个temp空间
O(N^2)
for(int i=0;i<array.length-1;i++){
for(int j=0;j<array.length-1-i;j++){
if(array[j]>array[j+1]){
int temp=array[j];
array[j]=array[j+1];
array[j+1]=temp;
}
}
优化过的代码:O(N)
二分查找:log2N;(下面的情况是数组有序,况且我们找的是数组元素最大的位置下标)
2个元素------>找两次
四个元素----->找3次
八个元素------>找4次
我们把元素个数设置成N,我们把比较的次数设置成a,从上述关系可以得出2^(a-1)=n,所以我们最终算出的a=logN+1,每一次走完都砍掉一半
这里面的比较是等同于left=mid+1,right=mid-1;比较是不会消耗时间复杂度的,只有计算才会
我们的二分查找最坏的情况下就是来进行查找最后一个元素,这时候进行查找所需要的时间是最慢的,比较次数先不算
long factorial(int N) { return N < 2 ? N : factorial(N-1) * N; } O(N)----->假设现在N传递的是4,那么就进行了4次递归,每一次递归就进行以此计算 不是循环 画图理解: 4*f(3) 3*f(2) 2*f(1) 1 总共递归了三次,每一次计算了一次,所以他的时间复杂度就是: 递归的次数*每一层递归执行的次数=N*1=N
// 计算阶乘递归factorial的时间复杂度?
//求斐波那契数列的时间复杂度
int fibonacci(int N) {
return N < 2 ? N : fibonacci(N-1)+fibonacci(N-2);
}
2^N
f(4)(执行两次)
f(3)(执行两次) f(2)(执行一次)
f(2)(执行1次) f(1)(执行0次) f(1)(执行0次)
f(1)
总共执行了7次数一数也就等于元素个数
最坏的情况下是每一层的树都是满的
f(0)---->1--->2^0
f(1)---->2--->2^1
f(2)---->4---->2^2
上面那个斐波那契数列:2^0+2^1+2^2+2^3+.......2^(N-1)=2^N+1
(想想快排,每一层递归,我们事先进行递归左边,再递归右面,都要保存一块空间来进行记录基准值,每一层这一块空间都是一样的)
递归的时间复杂度--->递归的次数*每一层递归语句执行的次数(每一层必须都是一样的执行次数)
空间复杂度:找额外的数据空间,对一个算法运行过程中临时额外占用存储空间的大小
必须占用的空间就不需要继续计算空间复杂度;
冒泡排序的时间复杂度是O(1)------没有随着问题规模的变大,你的变量在不断的增加,我们始终的boolean值在每一次循环都是占用的一份空间,但是说你给N个数据进行排序,这N个数据都没有地方进行存放,那么该如何进行排序呢?这个数组的长度是必须要的
public static long[] func(int n){ long[] array=new long[n+1]; array[0]=1; array[1]=1; for(int i=2;i<=n;i++){ array[i]=array[i-1]+array[i-2]; } return array; }
这个代码的时间复杂度就是O(N),因为随着你计算次数N的增多,所进行需要的数组长度也在变大,你要把自己计算的数据存放在数组里面
递归:每进行递归一次,都要在栈上面开辟一块内存,每一个函数都要开辟内存
递归调用了N次,开辟了N个栈祯,每一个栈祯能使用了常数个空间,所以他的时间复杂度是O(N)
// 计算阶乘递归的时间复杂度?
long factorial(int N) {
return N < 2 ? N : factorial(N-1) * N;
}
O(N)
//冒泡排序的时间复杂度是O(1)
顺序表
将数组写到类里面,就可以面向对象了,实现了很多功能,这个类可以提供很多方法对数组进行增删改查,就可以直接通过类实例化来进行操纵数组了
线性表:是具有N个相同特性的数据元素的有限集合,线性表是一种广泛应用的数据结构,常见的线性表有顺序表,链表,栈,队列,字符串,在逻辑上是连续的一条直线,但是在物理结构上面并不是连续的,线性表在物理上面存储的时候,通常以数组或链表的链式存储
1.顺序表和数组是不一样的,在JAVA里面你直接new了一个数组,他是不可以进行面向对象的,我们要让他能够面向对象,提供若干个方法来对数组进行增删改查 将数组作为一个属性,放到一个类里面,将来我面向对象了,我用这个类new出来的对象,通过这个对象来进行调用方法,就可以进行操作数组了 集成于操作数组的方法和数组本身的类,然后就可以使这个数组也能面向对象了 2.更方便的让他面向对象,在面向对象之后,可以定义一个额外的变量来进行保存数组的长度,不能遍历数组来计算个数,万一遇到0就是咱们本身的数据呢,所以说面向对象之后我们可以更方便计算统计有效数据
1)我现在new 了一个数组,长度是10,我现在要放以下元素22,33,44,55,66,77,现在我开始进行存放,况且你要进行操作数组,如果不让他面向对象,方法要重写很多次(假设在代码的第一行写了遍历数组,我们删除数组中的一个元素,又要遍历数组,这样遍历数组的操作就写了两遍)
2)我放了22,33,44之后我想要知道数组的有效元素的个数怎么办?
有的人可能会想,我放了22,33,44之后,后面的元素不都默认是0了吗?我现在进行遍历数组,使用一个count变量,来进行++,遇到0就进行返回,但是如果我此时放的元素是0,22,33,44,那么此时与遍历返回的count变量是0,那么这显然是不正确的;
所以数组中的元素恰好是0就不行,所以一个单独的数组是不可以进行有效数据的统计的
3)我已我们需要有一个整型变量数据来进行记录数组的个数;
1)顺序表是一段物理地址连续的存储单元依次储存数据元素的线性结构,一般情况下采用数组进行储存,在数组上完成对数据的增删改查
2)顺序表可以分为静态顺序表(使用定长数组来进行存储)和动态顺序表(使用动态开辟的数组存储)
3)静态顺序表主要是适用于确定知道要存多少数据的场景,否则开辟的空间大了浪费,开辟的空间小了还不够用
1.在指定位置进行新增元素的时候:
package com.example.demo;
import java.util.Arrays;
public class MyArrayList {
public int[] array;
public int count;
public MyArrayList(int n){
this.array=new int[n];
}
//获取顺序表的有效数据长度
public int GetCount(){
return this.count;
}
//遍历整个顺序表的元素
public void display(){
for(int i=0;i<this.count;i++){
System.out.println(array[i]);
}
}
//在指定的pos位置添加元素,在顺序表中所进行插入的位置的元素前面一定是已经是有有效数据的
//况且我们在count位置插入元素也是可以的
public void add(int pos,int data){
//1.先判断pos的合法性
if(pos<0||pos>count){//在这里面等于count也是可以的
throw new UnsupportedOperationException("当前你要插入的数组下标位置不合法");
}
//2.判断数组长度是否满了,满了以2倍来进行扩容
if(pos==array.length){
this.array= Arrays.copyOf(array,this.array.length*2);
//在这里面我们一定要注意,这个Arrays.copyOf市创建了一个新的数组,是原数组的2倍
//我们再让老的数组的引用指向新的数组元素的对象
}
//3.进行插入元素之前,先进行移动元素
for(int i=count-1;i>=pos;i--){
this.array[i+1]=this.array[i];
}
//4.进行存放元素
array[pos]=data;
//5.计数器++
count++;
}
public boolean contains(int data){
for(int i=0;i<array.length;i++){
if(this.array[i]==data){
return true;
}
}
return false;
}
public static void main(String[] args) {
MyArrayList list=new MyArrayList(10);
list.add(0,1);
list.add(1,2);
list.add(2,3);
list.add(3,4);
list.add(0,10);
list.display();
}
}
2)获取到指定位置的元素:
public int get(int pos){//获取到pos位置下标的元素
//1.先进行判断pos位置下标是否合法
if(pos<0||pos>=array.length){
throw new UnsupportedOperationException("pos位置元素下表不合法");
}
//2.判断数组的长度是否为空:
if(count==0){
throw new UnsupportedOperationException("数组长度为空");
}
//3.返回数据
return array[pos];
}
3)更新操作:设置pos位置的元素为value
public void update(int pos,int data){ //1.先进行判断元素下标pos是否合法 if(pos<0||pos>array.length){ throw new UnsupportedOperationException("当前设置的元素为孩子的下标不合法"); } //2.顺序表为空 if(count==0){ throw new UnsupportedOperationException("数组长度为空"); } //3.进行设置 array[pos]=data; }
4)删除操作,我们在进行删除元素的时候,要考虑到以下几个点;
考虑的条件:
1)看一下顺序表是否为空
2)查找你要进行删除的数字
3)定一下标进行删除操作:array[i+1]=array[i]
如果说数组长度恰好满了,那么i<count的话会发生数组越界异常,所以最后的条件是i<usedsize-1;如果是引用数据类型,那么必须手动置为空,保证这个对象没有人引用;
4)计数器减减
1)先进行判断顺序表是否为空,也就是说顺序表中是否有元素
2)查找你要删除的数字的下标,也就是说判断元素是否在顺序表中存在,下标不存在,直接返回
3)arr1[i]=arr1[i+1];条件i<usedsize-1,加上如果说数组满了
那么就把如果说i走到了usedsize-1的位置,那么就把i+1的的下标的值赋值给i下标
此时就会报出空指针异常
public void remove(int data)
{
if(usedsize==0)
{
System.out.println("数组里面没有数据是不可以进行删除的");
return;
}
int index=search(data);
if(index==-1)
{
System.out.println("当下的数据在顺序表中是不存在的");
return;
}
//程序能走到这里,说明已经找到了要删除的数字,即将进行删除
for(int i=index;i<usedsize-1;i++)
{
arr1[i]=arr1[i+1];//这里面的i的值不可以写成index,arr1[index]=arr1[index+1]
}
//arr1[usesize]=null;是引用类型,那么一定要手动置为空,因为这个引用一定会指向一个对象,把引用值为空,那么就没有引用指向了这个对象
usedsize--;
}
5)清空所有元素:
public void clear() { usedsize=0; //如果说这里面数组的元素全部是引用数据类型,那么我们必须遍历这个数组,把所有的引用置为空 //for(int i=0;i<usedsize;i++) // { // arr1[usedsize]=null; // } }
数据结构=数据+结构,数据的种类不同,说明描述和组织数据的方式是不一样的
顺序表总结:
1)插入和删除元素的时候,必须得进行移动元素
2)扩容则会导致空间的浪费,拷贝数据,释放旧空间,会有不少的消耗,比如说现在顺序表有10个元素况且容量是10,我们现在以2倍进行扩容,假设现在此时只放了11个,那么此时存放了10个,那么就会导致元素空间的浪费
能不能插入和删除做到不移动元素做到O(1)
3)能不能随用随取,放一个给你一个空间
1)顺序表在物理和逻辑上面都是连续的
2)但是链表在物理上不一定连续,但是在逻辑上面是连续的
3)所以说链表是一种物理存储结构上非连续存储结构,但是数据元素的逻辑顺序是通过链表中的引用链接次序来进行实现的