一、数据结构和算法概述
1.1什么是数据结构?
数据结构就是把数据元素按照一定的关系组织起来的集合,用来组织和存储数据。
1.2数据结构分类
传统上,我们可以把数据结构分为逻辑结构和物理结构两大类。
逻辑结构分类:
a.集合结构:集合结构中数据元素除了属于同一个集合外,他们之间没有任何其他的关系。
b.线性结构:线性结构中的数据元素之间存在一对一的关系
c.树形结构:树形结构中的数据元素之间存在一对多的层次关系
d.图形结构:图形结构的数据元素是多对多的关系
物理结构:
顺序存储结构(数组) -->把数据元素放到地址连续的存储单元里面,便于寻址。
链式存储结构(链表)。 -->存储单元可以是连续的也可以是不连续的,通过指针存放数据元素的地址,来查找相关联数据元素的位置。
1.3什么是算法?
根据一定的条件,对一些数据进行计算,得到需要的结果。
算法的两个目标:
1.花最少的时间完成需求;
2.占用最少的内存空间完成需求;
二、算法分析
1.1算法的时间复杂度分析
事后分析估算方法、事前分析估算方法(主要依赖于算法的好坏和问题的输入规模)。
1.2函数渐近增长
概念:给定两个函数f(n)和g(n),如果存在一个整数N,使得对于所有的n>N,f(n)总是比g(n)大,那么我们说f(n)的增长渐近快于g(n)。
总结:
1.算法函数中的常数可以忽略;
2.算法函数中最高次幂的常数因子可以忽略;
3.算法函数中最高次幂越小,算法效率越高。
1.3算法时间复杂度
时间复杂度的一个总结:
复杂程度从低到高依次为:O(1)<O(logn)<O(n)<O(nlogn)<O(n2)<O(n3)
1.4java中常见内存占用
1.基本数据类型内存占用情况:
2.计算机访问内存的方式都是一次一个字节
3.一个引用(机器地址)需要8个字节表示:
例如: int a ;,则a这个变量需要占用8个字节来表示
4.创建一个对象,比如new Date(),除了Date对象内部存储的数据(例如年月日等信息)占用的内存,该对象本身也
有内存开销,每个对象的自身开销是16个字节,用来保存对象的头信息。
5.一般内存的使用,如果不够8个字节,都会被自动填充为8字节:
6.java中数组被被限定为对象,他们一般都会因为记录长度而需要额外的内存,一个原始数据类型的数组一般需要
24字节的头信息(16个自己的对象开销,4字节用于保存长度以及4个填充字节)再加上保存值所需的内存。
1.5算法的空间复杂度
算法的空间复杂度计算公式记作:S(n)=O(f(n)),其中n为输入规模,f(n)为语句关于n所占存储空间的函数。
案例:
对指定的数组元素进行反转,并返回反转的内容。
# 解法1
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;
}
#解法2
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中有内存垃圾回收机制,并且jvm对程序的内存占用也有优化(例如即时编译),我们无法精确的评估一
个java程序的内存占用情况,但是了解了java的基本内存占用,使我们可以对java程序的内存占用情况进行估算。java开发的,基本上都是服务器开发,普通情况下直接说复杂度,默认为算法的时间复杂度。
三、简单排序
1.1 Comparable接口介绍
1.定义一个学生类Student,具有年龄age和姓名username两个属性,并通过Comparable接口提供比较规则;
2.定义测试类Test,在测试类Test中定义测试方法Comparable getMax(Comparable c1,Comparable c2)完成测试
//学生类
public class Student implements Comparable<Student>{
private String username;
private int age;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"username='" + username + '\'' +
", age=" + age +
'}';
}
//定义比较规则
@Override
public int compareTo(Student o) {
return this.getAge()-o.getAge();
}
}
//测试类
public class Test {
public static void main(String[] args) {
Student stu1 = new Student();
stu1.setUsername("zhangsan");
stu1.setAge(17);
Student stu2 = new Student();
stu2.setUsername("lisi");
stu2.setAge(19);
Comparable max = getMax(stu1, stu2);
System.out.println(max);
}
//测试方法,获取两个元素中的较大值
public static Comparable getMax(Comparable c1,Comparable c2){
int cmp = c1.compareTo(c2);
if (cmp>=0){
return c1;
}else{
return c2;
}
}
}
1.1 冒泡排序(Bubble)
冒泡排序API设计:时间复杂度为O(N^2)
//排序代码
public class Bubble {
//对数组a中的元素进行排序
public static void sort(Comparable[] a){
for(int i=a.length-1;i>0;i--){
for (int j = 0; j <i; j++) {
if (greater(a[j],a[j+1])){
exch(a,j,j+1);
}
}
}
}
//比较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 t = a[i];
a[i]=a[j];
a[j]=t;
}
}
//测试代码
public class Test {
public static void main(String[] args) {
Integer[] a = {4, 5, 6, 3, 2, 1};
Bubble.sort(a);
System.out.println(Arrays.toString(a));
}
}
1.2 选择排序(Selection)
排序原理:时间复杂度为O(N^2)
1.每一次遍历的过程中,都假定第一个索引处的元素是最小值,和其他索引处的值依次进行比较,如果当前索引处的值大于其他某个索引处的值,则假定其他某个索引出的值为最小值,最后可以找到最小值所在的索引
2.交换第一个索引处和最小值所在的索引处的值
public class Selection {
//对数组a中的元素进行排序
public static void sort(Comparable[] a){
for (int i=0;i<a.length-1;i++){
//假定本次遍历,最小值所在的索引是i
int minIndex=i;
for (int j=i+1;j<a.length;j++){
if (greater(a[minIndex],a[j])){
//跟换最小值所在的索引
minIndex=j;
}
}
//交换i索引处和minIndex索引处的值
exch(a,i,minIndex);
}
}
//比较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 t = a[i];
a[i]=a[j];
a[j]=t;
}
}
//测试代码
public class Test {
public static void main(String[] args) {
Integer[] a = {4,6,8,7,9,2,10,1};
Selection.sort(a);
System.out.println(Arrays.toString(a));
}
}
1.3 插入排序(Insertion)
排序原理:时间复杂度为O(N^2)
1.把所有的元素分为两组,已经排序的和未排序的;
2.找到未排序的组中的第一个元素,向已经排序的组中进行插入;
3.倒叙遍历已经排序的元素,依次和待插入的元素进行比较,直到找到一个元素小于等于待插入元素,那么就把待
插入元素放到这个位置,其他的元素向后移动一位;
插入排序代码实现:
public class Insertion {
/*
对数组a中的元素进行排序
*/
public static void sort(Comparable[] a){
for (int i=1;i<a.length;i++){
//当前元素为a[i],依次和i前面的元素比较,找到一个小于等于a[i]的元素
for (int j=i;j>0;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 t = a[i];
a[i]=a[j];
a[j]=t;
}
}
二、高级排序
1.1希尔排序(Shell)
希尔排序是插入排序的一种,又称“缩小增量排序”,是插入排序算法的一种更高效的改进版本。
排序原理:
1.选定一个增长量h,按照增长量h作为数据分组的依据,对数据进行分组;
2.对分好组的每一组数据完成插入排序;
3.减小增长量,最小减为1,重复第二步操作。
增长量h的确定:增长量h的值每一固定的规则,我们这里采用以下规则:
int h=1
while(h<数组的长度/2){
h=2h+1;//3,7
}
//循环结束后我们就可以确定h的最大值;
h的减小规则为:
h=h/2
希尔排序的代码实现:
//排序代码
public class Shell {
/*
对数组a中的元素进行排序
*/
public static void sort(Comparable[] a){
int N = a.length;
//确定增长量h的最大值
int h=1;
while(h<N/2){
h=h*2+1;
}
//当增长量h小于1,排序结束
while(h>=1){
//找到待插入的元素
for (int i=h;i<N;i++){
//a[i]就是待插入的元素
//把a[i]插入到a[i-h],a[i-2h],a[i-3h]...序列中
for (int j=i;j>=h;j-=h){
//a[j]就是待插入元素,依次和a[j-h],a[j-2h],a[j-3h]进行比较,如果a[j]小,那么
交换位置,如果不小于,a[j]大,则插入完成。
if (greater(a[j-h],a[j])){
exch(a,j,j-h);
}else{
break;
}
}
}
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 t = a[i];
a[i]=a[j];
a[j]=t;
}
}
//测试代码
public class Test {
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.toString(a));
}
}
可以使用事后分析法对希尔排序和插入排序做性能比较。
希尔排序和插入排序性能比较测试代码:
public class SortCompare {
public static void main(String[] args) throws Exception{
ArrayList<Integer> list = new ArrayList<>();
//读取reverse_arr.txt文件
BufferedReader reader = new BufferedReader(new InputStreamReader(new
FileInputStream("reverse_shell_insertion.txt")));
String line=null;
while((line=reader.readLine())!=null){
//把每一个数字存入到集合中
list.add(Integer.valueOf(line));
}
reader.close();
//把集合转换成数组
Integer[] arr = new Integer[list.size()];
list.toArray(arr);
testInsertion(arr);//使用插入排序耗时:20859
// testShell(arr);//使用希尔排序耗时:31
}
public static void testInsertion(Integer[] arr){
//使用插入排序完成测试
long start = System.currentTimeMillis();
Insertion.sort(arr);
long end= System.currentTimeMillis();
System.out.println("使用插入排序耗时:"+(end-start));
}
public static void testShell(Integer[] arr){
//使用希尔排序完成测试
long start = System.currentTimeMillis();
Shell.sort(arr);
long end = System.currentTimeMillis();
System.out.println("使用希尔排序耗时:"+(end-start));
}
}
通过测试发现,在处理大批量数据时,希尔排序的性能确实高于插入排序。
1.2 归并排序(SortCompare)
递归:定义方法时,在方法内部调用方法本身,称之为递归.
注意事项:在递归中,不能无限制的调用自己,必须要有边界条件,能够让递归结束,因为每一次递归调用都会在栈内存开辟新的空间,重新执行方法,如果递归的层级太深,很容易造成栈内存溢出。
需求:请定义一个方法,使用递归完成求N的阶乘;
分析:
1!: 1
2!: 2*1=2*1!
3!: 3*2*1=3*2!
4!: 4*3*2*1=4*3!
...
n!: n*(n-1)*(n-2)...*2*1=n*(n-1)!
所以,假设有一个方法factorial(n)用来求n的阶乘,那么n的阶乘还可以表示为n*factorial(n-1)
代码实现:
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;
}
return n*factorial(n-1);
}
}
归并排序是建立在归并操作上的一种有效的排序算法,该算法是采用分治法的一个非常典型的应用。将已有序的子
序列合并,得到完全有序的序列;即先使每个子序列有序,再使子序列段间有序。若将两个有序表合并成一个有序
表,称为二路归并。
排序原理:时间复杂度为O(nlogn);
1.尽可能的一组数据拆分成两个元素相等的子组,并对每一个子组继续拆分,直到拆分后的每个子组的元素个数是1为止。
2.将相邻的两个子组进行合并成一个有序的大组;
3.不断的重复步骤2,直到最终只有一个组为止。
归并排序API设计:
归并原理图:
归并排序代码实现:
//排序代码
public class Merge {
private static Comparable[] assist;//归并所需要的辅助数组
/*
对数组a中的元素进行排序
*/
public static void sort(Comparable[] a) {
assist = new Comparable[a.length];
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;
}
int mid = lo + (hi - lo) / 2;
//对lo到mid之间的元素进行排序;
sort(a, lo, mid);
//对mid+1到hi之间的元素进行排序;
sort(a, mid+1, hi);
//对lo到mid这组数据和mid到hi这组数据进行归并
merge(a, lo, mid, hi);
}
/*
对数组中,从lo到mid为一组,从mid+1到hi为一组,对这两组数据进行归并
*/
private static void merge(Comparable[] a, int lo, int mid, int hi) {
//lo到mid这组数据和mid+1到hi这组数据归并到辅助数组assist对应的索引处
int i = lo;//定义一个指针,指向assist数组中开始填充数据的索引
int p1 = lo;//定义一个指针,指向第一组数据的第一个元素
int p2 = mid + 1;//定义一个指针,指向第二组数据的第一个元素
//比较左边小组和右边小组中的元素大小,哪个小,就把哪个数据填充到assist数组中
while (p1 <= mid && p2 <= hi) {
if (less(a[p1], a[p2])) {
assist[i++] = a[p1++];
} else {
assist[i++] = a[p2++];
}
}
//上面的循环结束后,如果退出循环的条件是p1<=mid,则证明左边小组中的数据已经归并完毕,如果退
出循环的条件是p2<=hi,则证明右边小组的数据已经填充完毕;
//所以需要把未填充完毕的数据继续填充到assist中,//下面两个循环,只会执行其中的一个
while(p1<=mid){
assist[i++]=a[p1++];
}
while(p2<=hi){
assist[i++]=a[p2++];
}
//到现在为止,assist数组中,从lo到hi的元素是有序的,再把数据拷贝到a数组中对应的索引处
for (int index=lo;index<=hi;index++){
a[index]=assist[index];
}
}
/*
比较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 class Test {
public static void main(String[] args) throws Exception {
Integer[] arr = {8, 4, 5, 7, 1, 3, 6, 2};
Merge.sort(arr);
System.out.println(Arrays.toString(arr));
}
}
归并排序的缺点:
需要申请额外的数组空间,导致空间复杂度提升,是典型的以空间换时间的操作。
1.3 快速排序
快速排序是对冒泡排序的一种改进。它的基本思想是:通过一趟排序将要排序的数据分割成独立的两部分,其中一
部分的所有数据都比另外一部分的所有数据都要小,然后再按此方法对这两部分数据分别进行快速排序,整个排序
过程可以递归进行,以此达到整个数据变成有序序列。
排序原理:
1.首先设定一个分界值,通过该分界值将数组分成左右两部分;
2.将大于或等于分界值的数据放到到数组右边,小于分界值的数据放到数组的左边。
3.然后,左边和右边的数据可以独立排序。对于左侧的数组数据,又可以取一个分界值,将该部分数据分成左右两
部分,同样在左边放置较小值,右边放置较大值。右侧的数组数据也可以做类似处理。
4.重复上述过程,可以看出,这是一个递归定义。通过递归将左侧部分排好序后,再递归排好右侧部分的顺序。当
左侧和右侧两个部分的数据排完序后,整个数组的排序也就完成了。
快速排序API设计:
切分原理:
把一个数组切分成两个子数组的基本思想:
1.找一个基准值,用两个指针分别指向数组的头部和尾部;
2.先从尾部向头部开始搜索一个比基准值小的元素,搜索到即停止,并记录指针的位置;
3.再从头部向尾部开始搜索一个比基准值大的元素,搜索到即停止,并记录指针的位置;
4.交换当前左边指针位置和右边指针位置的元素;
5.重复2,3,4步骤,直到左边指针的值大于右边指针的值停止。
快速排序代码实现:平均时间复杂度为O(nlogn),最优的时间复杂度为O(nlogn);最坏时间复杂度为O(n^2);
//排序代码
public class Quick {
public static void sort(Comparable[] a) {
int lo = 0;
int hi = a.length - 1;
sort(a, lo,hi);
}
private static void sort(Comparable[] a, int lo, int hi) {
if (hi<=lo){
return;
}
//对a数组中,从lo到hi的元素进行切分
int partition = partition(a, lo, hi);
//对左边分组中的元素进行排序
//对右边分组中的元素进行排序
sort(a,lo,partition-1);
sort(a,partition+1,hi);
}
public static int partition(Comparable[] a, int lo, int hi) {
Comparable key=a[lo];//把最左边的元素当做基准值
int left=lo;//定义一个左侧指针,初始指向最左边的元素
int right=hi+1;//定义一个右侧指针,初始指向左右侧的元素下一个位置
//进行切分
while(true){
//先从右往左扫描,找到一个比基准值小的元素
while(less(key,a[--right])){//循环停止,证明找到了一个比基准值小的元素
if (right==lo){
break;//已经扫描到最左边了,无需继续扫描
}
}
//再从左往右扫描,找一个比基准值大的元素
while(less(a[++left],key)){//循环停止,证明找到了一个比基准值大的元素
if (left==hi){
break;//已经扫描到了最右边了,无需继续扫描
}
}
if (left>=right){
//扫描完了所有元素,结束循环
break;
}else{
//交换left和right索引处的元素
exch(a,left,right);
}
}
//交换最后rigth索引处和基准值所在的索引处的值
exch(a,lo,right);
return right;//right就是切分的界限
}
/*
数组元素i和j交换位置
*/
private static void exch(Comparable[] a, int i, int j) {
Comparable t = a[i];
a[i] = a[j];
a[j] = t;
}
/*
比较v元素是否小于w元素
*/
private static boolean less(Comparable v, Comparable w) {
return v.compareTo(w) < 0;
}
}
//测试代码
public class Test {
public static void main(String[] args) throws Exception {
Integer[] arr = {6, 1, 2, 7, 9, 3, 4, 5, 8};
Quick.sort(arr);
System.out.println(Arrays.toString(arr));
}
}
1.4 堆排序
堆的定义
堆通常可以被看做是一棵完全二叉树的数组对象。
堆的特性:
- 它是完全二叉树,除了树的最后一层结点不需要是满的,其它的每一层从左到右都是满的,如果最后一层结点不是满的,那么要求左满右不满。
- 它通常用数组来实现。具体方法就是将二叉树的结点按照层级顺序放入数组中,根结点在位置1,它的子结点在位置2和3,而子结点的子结点则分别在位置4,5,6和7,以此类推。
堆排序
给定一个数组:String[] arr = {“S”,“O”,“R”,“T”,“E”,“X”,“A”,“M”,“P”,“L”,“E”}
1.4.1 API设计
1.4.2 堆构造过程
创建一个新数组,把原数组0~
length-1的数据拷贝到新数组的1~length处,再从新数组长度的一半处开始往1索引处扫描(从右往左),然后对扫描到的每一个元素做下沉调整即可。
//每个节点的元素大于等于子节点的元素,最大的元素为堆顶。
1.4.3 堆排序过程
对构造好的堆,我们只需要做类似于堆的删除操作,就可以完成排序。
1.将堆顶元素和堆中最后一个元素交换位置;
2.通过对堆顶元素下沉调整堆,把最大的元素放到堆顶(此时最后一个元素不参与堆的调整,因为最大的数据已经到了数组的最右边)
3.重复1~2步骤,直到堆中剩最后一个元素。
1.4.4 代码
//对排序代码
public class HeapSort {
//对source数组中的数据从小到大排序
public static void sort(Comparable[] source) {
//1.创建一个比原数组大1的数组
Comparable[] heap = new Comparable[source.length + 1];
//2.构造堆
createHeap(source,heap);
//3.堆排序
//3.1定义一个变量,记录heap中未排序的所有元素中最大的索引
int N = heap.length-1;
while(N!=1){
//3.2交换heap中索引1处的元素和N处的元素
exch(heap,1,N);
N--;
//3.3对索引1处的元素在0~N范围内做下沉操作
sink(heap,1,N);
}
//4.heap中的数据已经有序,拷贝到source中
System.arraycopy(heap,1,source,0,source.length);
}
//根据原数组source,构造出堆heap
private static void createHeap(Comparable[] source, Comparable[] heap) {
//1.把source中的数据拷贝到heap中,从heap的1索引处开始填充
System.arraycopy(source,0,heap,1,source.length);
//2.从heap索引的一半处开始倒叙遍历,对得到的每一个元素做下沉操作
for (int i = (heap.length-1)/2; i>0 ; i--) {
sink(heap,i,heap.length-1);
}
}
//判断heap堆中索引i处的元素是否小于索引j处的元素
private static boolean less(Comparable[] heap, int i, int j) {
return heap[i].compareTo(heap[j])<0;
}
//交换heap堆中i索引和j索引处的值
private static void exch(Comparable[] heap, int i, int j) {
Comparable tmp = heap[i];
heap[i] = heap[j];
heap[j] = tmp;
}
//在heap堆中,对target处的元素做下沉,范围是0~range
private static void sink(Comparable[] heap, int target, int range){
//退出条件:没有子结点了
while (2*target<=range){
//1.找出target结点的两个子结点中的较大值
int max=2*target;
if (2*target+1<=range){
//存在右子结点
if (less(heap,2*target,2*target+1)){
max=2*target+1;
}
}
//2.如果当前结点的值小于子结点中的较大值,则交换
if(less(heap,target,max)){
exch(heap,target,max);
}
//3.更新target的值
target=max;
}
}
}
//测试代码
public class Test {
public static void main(String[] args) throws Exception {
String[] arr = {"S", "O", "R", "T", "E", "X", "A", "M", "P", "L", "E"};
HeapSort.sort(arr);
System.out.println(Arrays.toString(arr));
}
}
1.5 计数排序
非比较排序,桶思想的一种。
算法思想:
- 量大但是范围小
- 某大型企业数万名员工年龄排序
- 如何快速得知高考名次
算法步骤
- 花O(n)的时间扫描一下整个序列 A,获取最小值 min 和最大值 max
- 开辟一块新的空间创建新的数组 B,长度为 ( max - min + 1)
- 数组 B 中 index 的元素记录的值是 A 中某元素出现的次数
- 最后输出目标整数序列,具体的逻辑是遍历数组 B,输出相应元素以及对应的个数
复杂度,时间、空间均为 O(n+k)
//Java 代码实现
public class CountingSort implements IArraySort {
@Override
public int[] sort(int[] sourceArray) throws Exception {
// 对 arr 进行拷贝,不改变参数内容
int[] arr = Arrays.copyOf(sourceArray, sourceArray.length);
int maxValue = getMaxValue(arr);
return countingSort(arr, maxValue);
}
private int[] countingSort(int[] arr, int maxValue) {
int bucketLen = maxValue + 1;
int[] bucket = new int[bucketLen];
for (int value : arr) {
bucket[value]++;
}
int sortedIndex = 0;
for (int j = 0; j < bucketLen; j++) {
while (bucket[j] > 0) {
arr[sortedIndex++] = j;
bucket[j]--;
}
}
return arr;
}
private int getMaxValue(int[] arr) {
int maxValue = arr[0];
for (int value : arr) {
if (maxValue < value) {
maxValue = value;
}
}
return maxValue;
}
}