数据结构与算法(一)

1、数据结构

数据结构是计算机存储,组织数据的方式,指相互之间存在一种或多种特定关系数据元素的集合。
通常情况下,精心选择的数据结构可以带来更高的运行或者存储效率。数据结构往往同高效的检索算法和索引技术有关

1.1、数据结构的基本功能
  • 如何插入一条新的数据项
  • 如何寻找某一特定的数据项
  • 如何删除某一特定的数据项
  • 如何迭代的访问各数据项,以便进行显示或其他操作
1.2、常用的数据结构
  • 数组Array
  • 栈Stack
  • 队列Queue
  • 链表Linkde List
  • 数Tree
  • 哈希表Hash
  • 堆Heap
  • 图Graph

2、算法

 算计简单来说就是解决问题的步骤
 在Java中,算法通常都是由类的方法来实现的。前面的数据结构,比如链表为啥插入、删除块,而查找慢,平衡的二叉树插入、删除、查找都快,这都是实现这些数据结构的算法所造成的。

2.1、算法的五个特征
  • 有穷性:对于任意一组合法输入值,在执行有穷步骤之后一定能结束,即:算法中的每个步骤都能在有限时间内完成。
  • 确定性:在每种情况下所应执行的操作,在算法中都有确定的规定,使算法的执行者或阅读者都能明确其含义及如何执行。并且在任何条件下,算法都只有一条执行路径。
  • 可行性:算法中的所有操作都必须足够基本,都可以通过已经实现的基本操作运算有限次实现。
  • 有输入:作为算法加工对象的量值,通常体现在算法当中的一组变量。有些输入量需要在算法执行的过程中输入,而有的算法表面上可以没有输入,实际上已被嵌入算法之中。
  • 有输出:它是一组与输入有确定关系的量值,是算法进行信息加工后得到的结果,这种确定关系即为算法功能。
2.2、算法的设计原则
  • 正确性:首先,算法应当满足以特定的规则说明方式给出的需求。其次,对算法是否正确的理解有以下四个层次:
     一、程序语法错误
     二、程序对于几组输入数据能够得出满足需求的结果
     三、程序对于精心选择的、典型、苛刻且带有刁难性的几组输入数据能够得出满足要求的结果
     四、程序对于一切合法的输入数据都能得到满足要求的结果
  • 可读性:算法为了人的阅读与交流,其次才是计算机执行。因此算法应该易于人的理解。另一方面,晦涩难懂的程序易于隐藏较多的错误而难以调试
  • 健壮性:当输入的数据非法时,算法应当恰当的做出反应或进行相应处理,而不是产生莫名其妙的输出结果。并且处理出错的方法不应是中断程序执行,而是应当返回一个表示错误或错误性质的值,以便在更高的抽象层次进行处理
  • 高效率与低存储量需求:通常算法效率指的是算法执行时间;存储量是指算法执行过程中所需要的最大存储空间,两者都与问题的规模有关。

 前面三点正确性,可读性和健壮性都好理解。对于第四点算法的执行效率和存储量,我们知道比较算法的时候,可能会说‘A算法比B算法快两倍’之类的话,但实际上这种说法没有任何意义。因为当数据项个数发生变化时,A算法和B算法的效率比例也会发生变化,比如数据项增加了50%,可能A算法比B算法快三倍,但是如果数据项减少了50%,可能A算法和B算法速度一样。所有描述算法的速度必须要和数据项的个数联系起来。

3、数组

3.1、Java数组介绍

  在Java中,数组是用来存放同一种数据类型的集合,注意只能存放同一种数据类型(Object类型除外)

数组的声明

第一种方式:

数据类型[] 数组名 = new 数据类型[数组长度];

这里[]可以放在数组名称的前面,也可以放在数组名称的后面,我们推荐放在数组名称的前面,这样看上去数据类型[] 表示的很明显是一个数组类型,放在数组名称之后则显得不是那么直观

第二种方式:

数据类型[] 数组名称 = {元素1,元素2};

这种方式声明数组的同时直接给定了数组的元素,数组的大小由给定的数组元素个数决定

//声明数组1,长度为1,存放int类型的数组
int[] array1 = new int[1];
//声明数组2,长度为3,数组元素为1,2,3的数组
int[] array2 = new {1,2,3};
访问数组元素以及给数组元素赋值

 数组是存在下标索引的,通过下标可以获取指定位置的元素,数组索引是从0开始的,也就是说索引0对应的就是数组中第一个元素,可以很方便的对数组中的元素进行存取操作。
 前面的数组的声明第二种方式,我们在声明数组的同时,也进行了初始化赋值

//声明一个长度为3,存放int类型的数组
int[] array1 = new int[3];
//给array1第一个元素赋值1
array1[0] = 1;
/访问array1的第一个元素
System.out.println(array1[0]);

 上面的array1数组,我们只能赋值三个元素,也就是索引从0到2,如果你访问array1[3],那么会报数组越界异常

java.lang.ArrayIndexOutOfBoundsException
数组遍历

 数组有个length属性,是记录数组的长度的,我们可以利用length属性来遍历数组

//声明一个元素为1,2,3的数组
int[] array = new {1,2,3};
//遍历数组
for( int i = 0 ; i < array.length ; i++) {
	System.out.println(array[i]);
}
3.2、用类封装数组实现数据结构

 前面我们介绍了一个数据结构必须具有以下的基本功能

  • 1.如何插入一条新的数据项
  • 2.如何寻找某一特定的数据项
  • 3.如何删除某一特定的数据项
  • 4.如何迭代的访问各个数据项,以便进行显示或其他操作
     而我们知道了数组的简单用法,现在用类的思想封装一个数组,实现上面的四个基本功能
    ps:假设不会添加重复元素,这里没有考虑重复元素
public class testArray {

    /**
     * 定义一个数组
     */
    private int[] intArray;

    /**
     * 定义数组的实际有效长度
     */
    private int elems;

    /**
     * 定义数组的最大长度
     */
    private int length;

    /**
     * 无参构造,构造一个长度为50的数组
     */
    public testArray(){
        elems= 0;
        length = 50;
        intArray = new int[length];
    }

    /**
     * 构造函数,初始化一个长度为length的数组
     * @param length
     */
    public testArray(int length){
        elems = 0;
        this.length = length;
        intArray = new int[length];
    }

    /**
     * 获取数组的有效长度
     * @return
     */
    public int getSize(){
        return elems;
    }

    /**
     * 遍历显示元素
     */
    public void display(){
        for (int i = 0; i < elems ; i++) {
            System.out.print(intArray[i]+" ");
        }
        System.out.println();
    }

    /**
     * 添加元素
     * @param   这里假设不会添加重复元素
     * @return  添加成功返回true,添加的元素超过范围返回false
     */
    public boolean add(int value){
        if (elems == length){
            return false;
        }else {
            intArray[elems] = value;
            elems++;
        }
        return true;
    }

    /**
     * 根据索引获取元素
     * @param i
     * @return  查找索引值在数组索引有效范围内,返回索引所表示的元素
     * 查找索引超出数组索引有效值,提示索引越界
     */
    public int get(int i){
        if (i<0 || i>elems){
            System.out.println("索引越界了");
        }
        return intArray[i];
    }

    /**
     * 根据元素返回索引值
     * @param searchValue
     * @return  查找的元素如果存在则返回索引值,如果不存在,返回-1
     */
    public int find(int searchValue){
        int i ;
        for (i = 0; i < elems ; i++){
            if(intArray[i] == searchValue){
                break;
            }
        }
        if(i == elems){
            return -1;
        }
        return i;
    }

    /**
     * 删除元素
     * @param value
     * @return  如果要删除的值不存在,直接返回false,否则返回true,删除成功
     */
    public boolean delete(int value){
        int k = find(value);
        if(k == -1){
            return false;
        }else {
            if(k == elems-1){
                elems --;
            }else {
                for (int i = k; i < elems -1 ; i++){
                    intArray[i] = intArray[i+1];
                }
                elems--;
            }
            return true;
        }
    }

    /**
     * 修改数据
     * @param oldValue  原值
     * @param newValue  新值
     * @return  修改成功返回true,失败返回false
     */
    public boolean modify(int oldValue,int newValue){
        int i = find(oldValue);
        if ( i == -1) {
            System.out.println("需要修改的数据不存在");
            return false;
        }else {
            intArray[i] = newValue;
            return true;
        }
    }

}

测试类:

public class myArrayTest {

    public static void main(String[] args) {
        //创建自定义封装数组结构,数组大小为4
        testArray array = new testArray(4);
        //添加4个元素分别是1,2,3,4
        array.add(1);
        array.add(2);
        array.add(3);
        array.add(4);
        //显示数组元素
        array.display();
        //查找索引为0的元素
        int i = array.get(0);
        System.out.println(i);
        //删除值为4的元素
        array.delete(4);
        //将元素值3改为33
        array.modify(3,33);
        array.display();
    }

}

结果为:
在这里插入图片描述

3.3、分析数组的局限性

 通过上面的代码,我们发现数组是能够完成一个数据结构索引的功能的,而且实现起来也不难,那数组既然能完成所有的工作,我们实际应用中为啥不用它来进行所有的数据存储呢?
 数组的局限性分析:
 1、插入快。对于无序数组,上面实现的数组就是无序的,即元素没有按照从小到大或者某个特定的顺序排列,只是按照插入的顺序排列。无序数组增加一个元素很简单,只需要在数组末尾添加元素即可,但是有序数组就不一样了,它需要在指定的位置插入。
 2、查找慢。当然如果根据索引来查找是很快的,但是我们通常都是根据元素值来查找的,给定一个元素值,对于无序数组,我们需要从数组第一个元素开始遍历,直到找到那个元素。有序数组通过特定的算法查找的速度会比无序数组快。
 3、删除慢。根据元素值删除,我们要先找到该元素所处的位置,然后将元素后面的值整体向前面移动一个位置。也需要比较多的时间
 4、数组一旦创建后,大小就固定了,不能动态扩展数组的元素个数。如果初始化一个很大的数组大小,那会白白浪费内存空间,如果给小了,后面数据个数增加了又添加不进去
 很显然,数组虽然插入快,但是查找和删除都比较慢,而且扩展性差,所以我们一般不会用数组来存储数据,那有没有什么数据结构插入,查找,删除都很快,而且还能动态扩展存储个数大小呢,答案是有的,但是这是建立在很复杂的算法基础上。

4、数组排序

1、冒泡排序

 这个名词的由来很好理解,一般水中的冒泡,水底刚冒出来的时候是比较小的,随着慢慢向水面浮起会逐渐增大。
 冒泡排序的运作规律如下:
 1、比较相邻的元素。如果第一个比第二个大,就交换二者
 2、对每一对相邻元素作相同的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素会是最大的数
 3、针对所有的元素重复以上的步骤,除了最后一个
 4、持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较
在这里插入图片描述
代码如下:

public class BubbleSort {

    public static int[] sort(int[] array){
        //这里for循环表示总共需要比较多少轮
        for (int i = 1; i < array.length; i++){
            //设定一个标记,若为true,则表示此次循环没有进行交换,也就是待排序列已经有序,排序已经完成
            boolean flag = true;
            //这里for循环表示每轮比较参与索引
            //对当前无序区间array[0,......length-1]进行排序
            //j的范围很关键,这个范围是在逐步缩小的,因为每轮比较都会将最大的放在右边
            for (int j = 0; j < array.length-1 ; j ++){
                if(array[j]>array[j+1]){
                    int temp = array[j];
                    array[j] = array[j+1];
                    array[j+1] = temp;
                    flag = false;
                }
            }
            if(flag){
                break;
            }
            //第i轮排序的结果:
            System.out.print("第"+i+"轮排序的结果为");
            display(array);
        }
        return array;
    }

    //遍历显示数组
    public static void display(int[] array){
        for (int i = 0 ; i < array.length ; i++){
            System.out.print(array[i]+" ");
        }
        System.out.println();
    }

    public static void main(String[] args) {
        int[] array = {4,2,8,9,5,7,6,1,3};
        System.out.println("未排序数组顺序为:");
        display(array);
        System.out.println("-----------------------------");
        array = sort(array);
        System.out.println("冒泡排序后的数组顺序为:");
        display(array);
    }
}

结果如下:
在这里插入图片描述
本来应该是8轮排序的,这里我们只进行了7轮排序,因为第7轮之后就是有序数组了

解释:

 冒泡排序是由两个for循环构成,第一个for循环的变量i表示总共需要多少轮比较,第二个for循环的变量j表示参与比较的元素索引【0,1,…,length-1】,因为每轮比较都会出现一个最大值放在最右边,所以每轮比较后的元素个数都会少一个,这也是为什么j范围是逐渐缩小的

分析:

 假设参与比较的数组元素个数为N,则第一轮排序有N-1次比较,第二轮有N-2次,以此类推,这种序列的求和公式为:
  (N-1)+(n-2)+…+1=N*(N-1)/2

 当N的值很大时,算法比较次数约为N2/2次比较,忽略减1

 假设数据是随机的,那么每次比较可能要交换位置,可能不会交换,假设概率为50%,那么交换次数为N2/4。不过如果是最坏的情况,那么每次比较都要交换位置

 交换和比较次数都和N2成正比,由于常数不算大O表示法中,忽略2和4,那么冒泡排序运行都需要(N2)时间级别

 其实无论何时,只要看见一个循环嵌套在另一个循环中,我们都可以怀疑这个算法的运行时间为O(N2)级,外层循环执行N次,内存循环对每一次外层循环都执行N次(或者几分之N次),这就意味着大约需要执行N2次基本操作

2、选择排序

 选择排序是每一次从待排序的数据元素中选出最小的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。
 分为三步:
 1、从带排序序列中,找到最小的元素
 2、如果最小元素不是待排序序列的第一个元素,将其和第一个元素互换
 3、从余下的N-1个元素中,找出最小的元素,重复前面步骤,直到排序结束
在这里插入图片描述
代码如下:

public class ChoiceSort {

    public static int[] sort(int[] array){
        //总共要经过N-1轮比较
        for (int i = 0; i < array.length ; i ++){
            int min = i;
            //每轮需要比较的次数
            for (int j = i+1; j <array.length ; j++){
                if(array[j]<array[min]){
                    min = j;    //记录每轮最小值元素的索引
                }
            }
            //将找到的最小值和i位置所在的值进行交换
            if(i!=min){
                int temp = array[i];
                array[i] = array[min];
                array[min] = temp;
            }
            System.out.println("第"+(i+1)+"轮排序后的结果为:");
            display(array);
        }
        return array;
    }

    //遍历显示数组
    public static void display(int[] array){
        for(int i = 0; i < array.length; i ++){
            System.out.print(array[i]+" ");
        }
        System.out.println();
    }

    public static void main(String[] args) {
        int[] array = {4,2,8,9,5,7,6,1,3};
        System.out.println("未排序数组为:");
        display(array);
        System.out.println("------------------------------");
        array = sort(array);
        System.out.println("------------------------------");
        System.out.println("选择排序后的数组为:");
        display(array);
    }
}

运行结果:
在这里插入图片描述

性能分析:

 选择排序和冒泡排序执行了相同次数的比较:N*(N-1)/2,但是至多只进行了N次交换
 当N值很大时,比较次数是主要的,所以和冒泡排序一样,用大O表示是O(N2)时间级别。但是由于选择排序交换的次数少,所以选择排序无疑是比冒泡排序快的。当N值较小时,如果交换时间比选择时间大得多,那么选择排序是相当快的

3、插入排序

 插入排序基本思想就是每一步将一个待排序的记录,插入到前面已经排好序的有序序列中去,直到插完所以元素为止。
 插入排序还分为直接插入排序,二分插入排序,链表插入排序,希尔排序等待,这里只是以直接插入排序讲解
在这里插入图片描述
代码如下:

public class InsertSort {

    public static int[] sort(int[] array){
        int j;
        //从索引为1的元素开始选择合适的位置插入,因为索引为0的只有一个元素,默认是有序的
        for (int i = 1; i < array.length ; i++){
            int temp = array[i];    //记录要插入的数据
            j = i;
            while(j>0 && temp < array[j-1]){    //从已经排序的序列最右边的开始比较,找到比其小的
                array[j] = array[j-1];  //向后挪动
                j--;
            }
            array[j] = temp; //存在比其小的数,插入
        }
        return array;
    }

    //遍历显示数组
    public static void display(int[] array){
        for(int i = 0; i < array.length; i ++){
            System.out.print(array[i]+" ");
        }
        System.out.println();
    }

    public static void main(String[] args) {
        int[] array = {4,2,8,9,5,7,6,1,3};
        System.out.println("未排序数组为:");
        display(array);
        System.out.println("------------------------------");
        array = sort(array);
        System.out.println("------------------------------");
        System.out.println("插入排序后的数组为:");
        display(array);
    }
}

运行结果:
在这里插入图片描述

性能分析:

 在第一轮排序中,它最多比较一次,第二轮最多比较两次,以此类推,第N轮,最多比较N-1次,以此有1+2+3+…+N-1=N*(N-1)/2

 假设在每一轮排序发现插入点时,平均只有全体数据项的一半进行了比较,除以2得到N*(N-1)/4.用大O表示法大致需要O(N2)时间级别

  复制的次数大致等于比较的次数,但是一次复制与一次交换的时间耗时不同,所以相对于随机数据,插入排序比冒泡快一倍,比选择排序略快。如果要进行逆序排列,那么每次比较和移动都会进行,这时候并不会比冒泡排序快

 冒泡,选择,插入用大O表示法都需要O(N2)时间级别,一般不会选择冒泡排序,虽然冒泡排序书写是最简单的,但是平均性能是没有选择排序和插入排序好的。
 选择排序把交换次数降低到最低,但是比较次数还是挺大的,当数据量小,并且交换数据相当于比较数据更加耗时的情况下,可以应用选择排序
 在大多数情况下,假设数据量比较小或者基本有序时,插入排序是三种算法中最好的选择。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值