文章目录
Java数据结构与算法
近期学习了些数据结构与算法的内容,对于笔记做一个记录,方便以后回看修改
学习资料全部来源于网络视频,这章先记录关于排序的实现
基本数据结构
数据结构分为"线性结构"和"非线性结构"
逻辑结构分类:
"集合结构":结构中的数据元素"除了同属于一种类型外,别无其它关系"
"线性结构":结构中的数据元素之间存在"一对一"的关系
"树型结构" :结构中的数据元素之间存在"一对多"的关系
"图状结构或网状结构":结构中的数据元素之间存在"多对多"的关系
物理结构分类:
"顺序存储结构":用数据元素在存储器中的相对位置来表示数据元素之间的逻辑关系。
"链式存储结构":在每一个数据元素中增加一个存放地址的指针,用此指针来表示数据元素之间的逻辑关系
算法和时间复杂度:
1.算法函数中的常数可以忽略
2.算法函数中最高次幂的常数因子可以忽略
3.算法函数中最高次幂越小,算法效率越高
"执行次数=执行时间"
一般使用大O表示法
1.用常数1取代运行时间中所有加法常数 例如 3次 记作O(1)
2.在修改后的运行次数中,只保留高阶项 例如: n^2+n次 记作 O(n^2)
3.如果最高阶项存在,且常数因子不为1,则去除与这个项相乘的常数
例如: 2n^2+3次 记作 O(n^2)
"时间复杂度比较"
O(1) < O(logn) < O(n)< O(nlogn)< O(n^2)< O(n!)< O(n^n)
代码中的空间
1.计算机访问内存的方式是一个字节一个字节的访问的
2.一个引用(机器地址)需要8个字节来表示
例如: Date date = new Date(), 则变量date需要占用8个字节位表示
3.创建一个对象,自身的开销是16字节,用来保存对象头部信息
4.一般内存的使用,如果不足8个字节,都会自动填充为8字节
例: public class A{
public int a=1;
}
通过new A()创建对象的内存:
1.整型成员变量 a占用4个字节
2.对象本身16个字节
那么一共需要20个字节,但是会自动填充为8的倍数,即24字节
5.一个原始"数组"类型,一般需要24字节头信息(16字节自身开销,4字节长度,4字节填充字节)
再加上保存值所需的内存
排序
冒泡排序
代码实现
public class BubbleSort {
/*
对数组a中的元素进行排序
* */
public static void sort(Comparable[]a){
//初始元素最大索引为"数组长度-1",第一个元素索引为"0"
//它之前已经没有元素了,不需要再比较了,所以停止条件需要大于第一个元素的索引值0
for (int i = a.length-1; i >0; i--) {
for(int j=0;j<i;j++){
//比较索引j和索引j+1的值,并进行交互位置
if(greater(a[j],a[j+1])){
exchange(a,j,j+1);
}
}
}
}
/*
比较v元素是否大于w元素
* */
private static boolean greater(Comparable v,Comparable w){
//v.compareTo(w)返回true时,说v>w
return v.compareTo(w)>0;
}
/*
数组元素i和j交换位置
* */
private static void exchange(Comparable[] a, int i, int j){
Comparable temp;
temp = a[i];
a[i]=a[j];
a[j]=temp;
}
}
/**冒泡排序测试
* */
@Test
public void testBubbleSort(){
//Integer包装类实现了Comparable<Integer>接口
Integer[] arr = {4,5,6,3,2,1};
BubbleSort.sort(arr);
System.out.println(Arrays.toString(arr));//[1, 2, 3, 4, 5, 6]
}
时间复杂度分析
冒泡排序使用了双层for循环,其中内层循环体才真正完成排序
最坏情况下如果要排序的元素为{6,5,4,3,2,1}的逆序
那么比较次数为:
(N-1)+(N-2)+(N-3)+...2+1=(N^2-N)/2
元素交换次数:
(N^2-N)/2
总执行次数:
N^2-N
时间复杂度:
O(N^2)
选择排序
1.每一次遍历的过程汇总,都假设第一个索引处0的元素是最小值,和其他元素进行比较
找出最小元素所在的索引值,重点也是这句话:
"最小元素的所在的索引值"
假设minIndex=0,此时索引0处值为4,不断比较,一旦发现比4小的值,产生交换
最后发现最小值是1,此时minIndex=7,产生交换,索引0和索引7的值交换
此时索引0的值为1,索引7的值为4,外层控制循环++,假设minIndex=1
......
2.交换第一个索引处和最小值所在索引处的值
3.每次找到一个最小值后,都只在剩余的元素中进行比较,并且剩余的每个元素都会被比较
代码实现
public class SelectionSort {
/*对数组a中的元素进行选择排序
* */
public static void sort(Comparable[]a){
for (int i=0;i<=a.length-2;i++){
//定义一个变量,记录最小元素的所在索引值,默认为参与选择排序的元素中第一个元素的索引
int minIndex = i;
for(int j=i+1;j<=a.length-1;j++){
//比较最小索引minIndex处的值和j索引处的值
if(greater(a[minIndex],a[j])){
minIndex=j;//索引值交换
}
}
//交换最小元素所在索引minIndex处的值和索引i处的值
exchange(a,i,minIndex);
}
}
/*比较v元素是否大于w元素
* */
public static boolean greater(Comparable v,Comparable w){
return v.compareTo(w)>0;
}
/*数组元素i和j交换位置
* */
public static void exchange(Comparable[] a, int i, int j){
Comparable temp;
temp=a[i];
a[i]=a[j];
a[j]=temp;
}
}
/**选择排序测试
* */
@Test
public void testSelectSort(){
Integer[] arr = {4,6,8,7,9,2,10,1};
SelectionSort.sort(arr);
System.out.println(Arrays.toString(arr));//[1, 2, 4, 6, 7, 8, 9, 10]
}
时间复杂度分析
选择排序使用了双层for循环,其中外层完成了数据交换,内层循环完成了数据比较
数据比较次数:
(N-1)+(N-2)+(N-3)+...2+1 = (N^2-N)/2
数据交换次数:
N-1
时间复杂度:
(N^2-N)/2+(N-1) = (N^2+N-1)/2 = O(N^2)
插入排序
1.把所有的元素分为两组,已经排序和未排序的
2.找到未排序的组中第一个元素,向已经排序的组中进行插入
3.倒序遍历已经排序的元素,依次和待插入的元素进行比较,直到找到一个元素小于等于待插入元素
那么就把待插入元素放在这个位置,其他元素向后移一位
与冒泡排序其实很类似,冒泡排序也是每次交换完必定会产生已排序的和未排序的
但不同的是,冒泡排序需要和每个元素都进行比较,而插入排序是倒序遍历的,我们
已知倒序比较的第一个元素,一定是已排序元素组中最大的元素,一旦待插入的元素
比最大的元素还大的话,就没必要继续比较下去了,直接跳出循环.
例如下图:
第三趟排序,待插入的元素是10,10前面的元素都是已从小到大排序的,我们
只需要把10和4比较一次即可,而不用和元素4前面的元素继续比较
假设有这样一个数组为 list[]{1,2,3,4};
我们要对其进行升序排序(很显然 这里已经是符合要求的升序排列).
现在 假若我们用冒泡排序 大概流程会是这样:
1.先将1,2进行比较 无须替换 然后2,3比较 无须替换 然后3,4比较 无须替换 完成第一轮冒泡 比较次数为3次
2.将2,3进行比较 无须替换 然后3,4进行比较 无须替换 完成第二轮冒泡 比较次数为2次
3.将3,4进行比较 无须替换 完成第三轮冒泡 比较次数为1次
很显然 我们没有移动一次数字 但是却比较了6次
再来看看插入排序如何实现 其详细步骤为:
1.先设定1为有序区间 将2和1比较 无须移动 比较一次
2.此时1,2均为有序区间 将3和2进行比较 无须移动 直接跳出while循环 比较一次
3.此时1,2,3均为有序区间 将4和3进行比较 无须移动 直接跳出while循环 比较一次
很显然 我们也没有移动数字 但是只比较了3次
"针对部分有序的集合来说,插入排序要优于冒泡排序,而无序的情况下,它们的效率是一样的"
代码实现
public class InsertionSort {
/*对数组a中的元素进行选择排序
* */
public static void sort(Comparable[] a){
//因为第一个元素默认为已排序的,所以初始已排序处索引为1,最大为最大索引
for(int i=1;i<a.length;i++){
//待排序的元素j=i,由于是倒序遍历进行比较,所以j能插入最小的索引位置要大于0
for(int j=i;j>0;j--){
//如果索引j-1处的值大于索引j处的值,则交换
if(greater(a[j-1],a[j])){
exchange(a,j-1,j);
}else{//否则退出循环,如果不退出循环,程序将一直进行
break;
}
}
}
}
/*比较v元素是否大于w元素
* */
public static boolean greater(Comparable v,Comparable w){
return v.compareTo(w)>0;
}
/*数组元素i和j交换位置
* */
public static void exchange(Comparable[] a, int i, int j){
Comparable temp;
temp=a[i];
a[i]=a[j];
a[j]=temp;
}
}
/**插入排序测试
* */
@Test
public void testInsertion(){
Integer[] arr = {4,3,2,10,12,1,5,6};
InsertionSort.sort(arr);
System.out.println(Arrays.toString(arr));//[1, 2, 3, 4, 5, 6, 10, 12]
}
时间复杂度分析
使用双层for循环,内层循环完成排序代码,最坏情况下:
比较次数:
(N^2-N)/2
交换次数:
(N^2-N)/2
总执行次数:
N^2-N
时间复杂度:
O(N^2)
希尔排序
希尔排序是插入排序的一种,又称"缩小增量排序",是插入排序的更高效的版本
与插入排序对比,如果存在这样的情况:已排序的分组元素为 {2,5,7,9,10}
未排序的元素为{1,8},那么下一个待插入元素为1,我们需要拿着1从后往前,
依次和10,9,7,5,2进行交换位置,而希尔排序改进的就是,减少比较的次数
排序原理:
1.选定一个增长量h,按照增长量h作为数据分组的依据,对数据进行分组;
2.对分好组的每一组数据完成插入排序
3.减少增长量,最小为1,重复第二步操作
增长量h的确定规则:
int h=1;
while(h<数组的长度/2){
h=2h+1
}
//循环结束后后可以确定h的最大值
h=h/2;
代码实现
public class ShellSort {
/*对数组a中的元素进行希尔排序
* */
public static void sort(Comparable[] a){
//1.根据数组a的长度来确定增长量h的初始值
int h=1;
while(h<a.length/2){
h=h*2+1;
}
//2.希尔排序
while(h>=1){//h最小为1
//2.1找到待插入的元素,待插入的元素不能比增长量h小,因为那样没办法比较
for(int i=h;i<=a.length-1;i++){
//2.2把待插入的元素插入到有序数列中,j每次减去一个h,即j下一次需要插入的位置
for(int j=i;j>=h;j-=h){
//比较待插入元素a[j]的值和前一个有序数列a[j-h]的值
if(greater(a[j-h],a[j])){
exchange(a,j-h,j);
}else{
//待插入的元素已经找到了合适的位置(即这个元素比前一个元素大),结束循环
break;
}
}
}
//减少h的值
h=h/2;
}
}
/*比较v元素是否大于w元素
* */
public static boolean greater(Comparable v,Comparable w){
return v.compareTo(w)>0;
}
/*数组元素i和j交换位置
* */
public static void exchange(Comparable[] a, int i, int j){
Comparable temp;
temp=a[i];
a[i]=a[j];
a[j]=temp;
}
}
/**希尔排序测试
* */
@Test
public void testShellSort(){
Integer[] arr = {9,1,2,5,7,4,8,6,3,6};
ShellSort.sort(arr);
System.out.println(Arrays.toString(arr));//[1, 2, 3, 4, 5, 6, 6, 7, 8, 9]
}
时间复杂度分析
时间复杂度
O(n^(1.5)) < O(n^2)
这是一个大致的时间复杂度,证明过程超过本次学习范围
相对于普通的插入排序,性能更优
归并排序
归并排序是建立在归并操作上的一种有效排序算法,采用分治法的一个典型应用.
将已有序的自序列合并,得到完全有序的序列,即先使每个子序列有序,再使子序列段间有序,
最后合并成一个有序表.
排序原理:
1.尽可能的把一组数据拆分成两个元素相等的子组,并继续拆分,直到每个子组的个数都为一
2.将相邻的两个子组进行排序,合并成一个有序的大组(归并的前提条件就是需要归并的数组是有序的)
3.不断重复步骤二,直到最终只有一个组为止
代码实现
public class MergeSort {
//归并所需的辅助数组
private static Comparable[] assist;
/*对数组a中的元素进行排序
* */
public static void sort(Comparable[] a){
//初始化辅助数组assist
assist = new Comparable[a.length];
//定义一个low变量和high变量,分别记录最小索引和最大索引
int low = 0;
int high = a.length-1;
//调用sort重载方法,完成从low到high的元素排序
sort(a,low,high);
}
/*对数组a中从low到high的元素进行排序
* */
private static void sort(Comparable[] a, int low, int high){
//做安全性校验,当子序列中只有一个元素时结束递归
if(high<=low){
return;
}else{
//对low到high之间的数据分成两个组,定义中间索引mid
int mid = (low+high)/2;
//对两个分组分别递归调用进行单独排序
sort(a,low,mid);
sort(a,mid+1,high);
//最后将两个组中数据进行归并
merge(a,low,mid,high);
}
}
/*对数组中,从low到mid为一组,从mid+1到high为一组,对两组数据进行归并
* */
public static void merge(Comparable[] a, int low, int mid, int high){
//定义三个指针,p1,p2分别指向两个数组的初始索引,i则指向辅助数组的初始索引
int i = low;
int p1 = low;
int p2 = mid+1;
//遍历,移动p1,p2指针,比较索引处的值,找出小的那个,放到对应索引处
while (p1<=mid && p2<=high){
if(less(a[p1],a[p2])){
assist[i++] = a[p1++];
}else{
assist[i++] = a[p2++];
}
}
//还需要考虑两种情况,任意一个数组元素先复制完成,下面的循环会执行其中一个
//如果上面循环退出条件是p1<=mid,则说明左组归并完毕,如果退出条件是p2<=high,则说明,右组归并完成
//遍历,如果指针p1没有走完,而p2已经走完,没有元素了,则顺序移动p1,把元素放到辅助数组对应索引处
while (p1<=mid){
assist[i++] = a[p1++];
}
//遍历,如果指针p2没有走完,而p1已经走完,没有元素了,则顺序移动p2,把元素放到辅助数组对应索引处
while (p2<=high){
assist[i++] = a[p2++];
}
//把辅助数组assist中的元素拷贝到原数组中
for(int index=low; index <= high; index++){
a[index] = assist[index];
}
}
/*比较v元素是否小于w元素
* */
private static boolean less(Comparable v,Comparable w){
return v.compareTo(w)<0;
}
}
时间复杂度分析
假设元素的个数是n,那么需要拆分log2(n)次,共log2(n)层
自顶向下第K层有2^K个子数组,每个数组长度为2^(3-k),归并最多需要比较2^(3-k)次比较
每层需要比较2^K次,K层则是K*2^K次比较次数
时间复杂度:
O(nlog2n)
缺点:
需要申请额外的数组空间,导致空间复杂度提示,是用空间换时间
快速排序
快速排序是对冒泡排序的一种改进
基本思想:
通过一趟排序将要排序的数据以一个分界值,分割成独立的两部分,其中分界值左边数据都比分界值右边数据小
然后重复此方法,对两部分数据分别快速排序,递归进行,达到整个数据变成有序
排序原理:
1.设定分界值,通过分界值将数组分为左右两部分
2.将大于或等于分界值的数据放数组右边,小于的放左边
3.重复步骤二,做类似递归处理
核心方法在于如何进行分组交换
//首先定义两个指针,分别指向待切分元素的最小索引处和最大索引下一个位置(指针其实就是索引的抽象表示)
int left =low;
int right = high+1;
确定分界值key为数组第一个元素后,分组方法中做了三件事
1.通过while循环控制右指针扫描,找到比key小的元素,即停下,跳出这个循环
2.进行第二个while循环,找到比key大的元素,指针停下,跳出这个循环
3.条件判断,左指针是否大于或者等于右指针的索引了,如果是,说明扫描完毕了
如果不是,则交换左指针和右指针的值,并且继续重复上面步骤
最后结束整个循环后,把分界值和左右指针指向的位置进行交换
代码实现
public class QuickSort {
/*对数组内元素进行快速排序*/
public static void sort(Comparable[] a){
int low=0;
int high=a.length-1;
sort(a,low,high);
}
/*对数组a中从索引low到high之间的元素进行排序*/
private static void sort(Comparable[] a, int low, int high){
if(low>=high){
return;
}else{
//进行分组,分为左子组和右子组
int par = partition(a, low, high);//返回分界值变换后的索引
//让左子组有序
sort(a,low,par-1);
//让右子组有序
sort(a,par+1,high);
}
}
/**核心部分*/
/*对数组a中,从索引low到high之间的元素进行分组,并且返回分组界限对应的索引*/
public static int partition(Comparable[]a, int low, int high){
//确定分界值
Comparable key = a[low];
//定义两个指针,分别指向待切分元素的最小索引处和最大索引下一个位置
int left =low;
int right = high+1;
//切分
while (true){
//先从右往左扫描,移动right指针,目标找到一个比分界值小的元素
while (greater(a[--right],key)){//右指针扫描元素比分界值key大的话,说明右指针没有找到,需要继续移动
if(right==low){
break;
}
}
//再从左往右扫描,移动left指针,目标找到一个比分界值大的元素
while (greater(key,a[++left])){//分界值key比左指针扫描元素大的话,说明左指针没有找到,需要继续移动
if(left==high){
break;
}
}
//判断left>=right,如果是,说明扫描完毕,结束循环,如果不是,则交换元素
if(left>=right){
break;
}else{
exchange(a,left,right);//left指针扫描比key大的元素,right指针扫描比key小的元素,然后交换
}
}
//交换分界值
exchange(a,low,right);//分界值即left或者right,因为它们指向同一个位置
return right;
}
private static boolean greater(Comparable v, Comparable w){
return v.compareTo(w)>0;
}
private static void exchange(Comparable[] a, int i, int j){
Comparable temp;
temp=a[i];
a[i]=a[j];
a[j]=temp;
}
}
时间复杂度分析
快速排序和归并排序,都是分治思想中的排序算法
把一个大问题分成若干小问题,最后将小问题的解法组成大问题的解法
区别:
1.快速排序不需要备用数组,直接在待排序序列中排序,而归并排序需要
2.快速排序是在两个子数组都有序的时候归并,归并完后直接是有序的
而归并排序将有序子数组归并后,还要进行排序,才能使整个数组有序
3.归并排序,数组被等分,基准值就是数组中间值
快速排序,切分数组的位置取决于数组的内容
时间复杂度分析:
最优情况:每一次切分选择的基准数字刚好和序列等分,切logn次,时间复杂度是O(logn)
最坏情况:每一次切分选择的基准数组刚好是数组的最大值或最小值,需要切n次,时间复杂度是O(n^2)
平均情况:时间复杂度O(nlogn)
排序的稳定性
稳定性的含义
数组arr中有若干元素,其中A元素和B元素相等,并且A元素在B元素前面,
如果使用某种排序算法排序后,能够保证A元素依然在B元素的前面,可以说这个该算法是稳定的
稳定性的意义
如果只是简单的进行数字的排序,那么稳定性将毫无意义
除非要排序的内容是一个复杂对象的多个数字属性,且其原本的初始顺序存在意义
那么我们需要在二次排序的基础上保持原有排序的意义
例如要排序的内容是一组原本按照价格高低排序的对象,如今需要按照销量高低排序,
使用稳定性算法,可以使得想同销量的对象依旧保持着价格高低的排序展现,只有销量不同的才会重新排序
这样可以减少系统的开销,因为如果下一次再进行价格高低排序的时候,可能会有之前已经排序数据,减少再排序
-------------------
如下图中,就是一种稳定的排序
稳定性比较
冒泡排序:
只有当arr[i]>arr[i+1]的时候,才会改变位置,相等的时候并不会改变,所以是稳定的
选择排序:
例如数据{5(1), 8, 5(2), 3, 9}里面有五个数,其中5是重复的,第一遍选择最小元素为3,
需要和第一个元素5(1)进行交换,则5(1)到了5(2)的后面,破坏了稳定性,所以是不稳定的
插入排序:
只有前一个元素a[j-1]大于待插入元素a[j]的时候,才会交换位置,等于的时候并不会交换
插入排序是稳定的
希尔排序:
希尔排序是对数据分组,每一组进行插入排序,如果进行多次插入,其稳定性可能会被打破
是不稳定的
归并排序:
只有当arr[i]<a[i+1]的时候,才会交换位置,元素相等也不会交换,所以是稳定的
快速排序:
快速排序需要一个基准值,例如数据{6,9,5(1),8,5(2)},此时,基准值为6,右指针移动扫描到值5(2),
左指针开始扫描,找到值9,这两个值交换后{6,5(2),5(1),8,9},这样两个5就发生了交换,破坏稳定性
总结
稳定排序: 冒泡,插入,归并
不稳定排序: 选择,希尔,快速