数据结构与算法学习与面试题目(记录学习笔记)未写完
-
数据结构概述
什么是数据结构?
数据结构是指相互之间存在着一种或多种关系的数据元素的集合和该集合中数据元素之间的关系组成(简单来说就是数据与数据之间的关系)。
数据结构研究的是数据的存储结构和数据的逻辑结构。
数据的存储结构:
顺序存储结构:是把数据元素存放在地址连续的存储单元里,其数据间的逻辑关系和物理关系是一致的。数组就是顺序存储结构的典型代表。
链式存储结构:为了表示每个数据元素与其直接后继元素之间的逻辑关系而创建的一种结构,结构除了需要存储数据元素本身的信息之外还需要存储其直接后继元素的地址信息。
顺序存储结构和链式存储结构的区别?
顺序存储结构的数据存储在连续的内存区域中,链式存储结构,逻辑上相邻的数据在物理存储上并不一定相邻,要访问使用链式存储结构存储的数据,需要使用数据地址进行访问。链式存储结构的数据并没有直接存储在变量的存储空间中,而是存储在另一个存储空间,变量的存储空间只是存储了另一空间的地址。
数据的逻辑结构:
数据的逻辑结构包含集合结构,线性结构,树形结构,图形结构。
集合结构:集合结构是数据元素同属于一个集合,他们之间是并列的关系,除此之外没有其他关系。
线性结构:线性结构中的元素存在一对一的相互关系。
树形结构:树形结构中的元素存在一对多的相互关系。
图形结构:图形结构中的元素存在多对多的相互关系。
-
算法的概述
算法的定义:
算法是指解题方案准确而完整的描述,是一系列解决问题的清晰指令,算法代表着用系统的方法描述解决问题的策略机制(简单来说就是算法就是一种解决问题的思路)。
算法的特性:输入,输出,有穷性,确定性,可行性。
算法的基本要求:正确性,可读性,健壮性,时间复杂度,空间复杂度。
算法没有最好的,只有最适合的。 -
线性结构
线性结构包含数组,栈,队列,单链表,循环链表,双链表。
3.1数组
数组的基本使用:
数组的长度不可变。但是可以通过新建数组再赋值给原数组的方式改变数组长度。
面向对象的数组:
public class MyArray {
//定义一个用于存储数据的数组
private int[] elements;
//初始化长度
public MyArray() {
elements = new int[0];
}
//获取数组长度的方法
public int size() {
return elements.length;
}
//往数组的末尾添加一个元素
public void add(int element){
//创建一个新数组,长度为原数组+1
int[] temp_arr = new int[elements.length+1];
//把原数组中的元素复制到新数组中
for(int i=0;i<elements.length;i++) {
temp_arr[i] = elements[i];
}
//把添加的元素放入到新数组中
temp_arr[elements.length] = element;
//替换成新数组
elements = temp_arr;
}
//打印所有元素到控制台
public void show() {
System.out.println(Arrays.toString(elements));
}
//删除数组中的元素
public void delete(int index) {
//判断下标是否越界
if(index < 0 || index > elements.length-1) {
throw new RuntimeException("下标越界");
}
//创建一个新数组,长度是原数组-1
int[] temp_arr = new int[elements.length-1];
//把原数组中除了要删除下标的元素之外的元素复制到新数组中
for(int i = 0; i < temp_arr.length;i++) {
//想要删除的元素前面的元素
if(i<index) {
temp_arr[i] = elements[i];
}
//想要删除的元素后面的元素
else {
temp_arr[i] = elements[i+1];
}
}
//将新数组替换旧数组
elements = temp_arr;
}
//取出指定位置的元素
public int get(int index) {
//判断下标是否越界
if(index < 0 || index > elements.length-1) {
throw new RuntimeException("下标越界");
}
return elements[index];
}
//插入一个元素到指定位置
public void insert(int index,int element) {
//判断下标是否越界
if(index < 0 || index > elements.length-1) {
throw new RuntimeException("下标越界");
}
//创建一个新数组,长度为原数组+1
int [] temp_arr = new int[elements.length+1];
for (int i = 0; i < elements.length; i++) {
//目标位置之前的元素
if(i<index) {
temp_arr[i] = elements[i];
//目标位置之后的元素
}else {
temp_arr[i+1] = elements[i];
}
}
//插入指定元素到指定位置
temp_arr[index] = element;
//新数组替换旧数组
elements = temp_arr;
}
//替换指定位置上的元素
public void set(int index,int element) {
//判断下标是否越界
if(index < 0 || index > elements.length-1) {
throw new RuntimeException("下标越界");
}
elements[index] = element;
}
}
数组的查找算法:
线性查找和二分法查找。
线性查找:遍历数组,每次都是从头开始查找目标元素,如存在停止,不存在就是将数组从头到尾遍历一遍。
public static void main(String[] args) {
//目标数组
int [] arr = new int [] {9,8,7,6,5,4,3,2,1};
//目标元素
int target = 5;
//目标元素所在的下标
int index = -1;
//遍历数组
for(int i = 0; i < arr.length; i++) {
if(arr[i] == target) {
index = i;
break;
}
}
System.out.println(index);
}
二分法查找:二分查找就是通过每次跟区间的中间元素进行对比,将待查询区域缩小为原来的一半,知道找到要查询的元素或者区间长度变为0。时间复杂度是 O(logn),也就意味着在亿级数据中查询一个数据可能只需要几十次。二分法查找性能虽然优秀,但是应用场景比较有限,底层必须依赖数组,并且要求数据必须是有序的。二分法查找更适合处理静态数据(没有频繁插入和删除操作)。
二分法查找的要求:
1.二分法查找底层依赖的是顺序存储结构,即是数组。
2.二分查找针对的是有序数据,二分查找只能应用在插入,删除操作不频繁,一次排序多次查找的场景中。
3.数据量太小不适合二分查找,数据量小可以直接遍历。
4.数据量太大也不适合二分查找,二分查找底层依赖数组这种数据结构,数组为了支持随机访问的特性,要求内存空间连续,对内存的要求比较苛刻,举个例子:如果我们需要用数组存储1G的数据,那么就需要内存分配1G连续的空间来存储这些数据。
public static void main(String[] args) {
//目标数组
int [] arr = new int[] {1,2,3,4,5,6,7,8,9};
//目标元素
int target = 8;
//目标元素的下标
int index = -1;
//记录开始位置
int begin = 0;
//记录结束位置
int end = arr.length-1;
//记录中间的位置
int mid = (begin+end)/2;
//循环查找
while(true) {
//判断中间那个元素是不是要查找的元素
if(arr[mid] == target) {
index=mid;
break;
//中间这个元素不是要查找的元素
}else {
//判断中间这个元素是不是比目标元素大
if(arr[mid] > target) {
//把结束位置调整到中间位置的前一个元素
end = mid-1;
}else {
//判断中间元素是不是比目标元素小
//把开始位置调整到中间位置的后一个位置
begin = mid + 1;
}
//取出新的中间位置
mid = (begin+end)/2;
}
}
System.out.println(index);
}
3.2栈
栈:又叫做堆栈,栈作为一种数据结构,是一种只能在一端进行插入和删除操作的特殊线性表,它按照先进后出的原则存储数据,先进入的数据被压入栈底,最后的数据在栈顶,需要读取数据的时候从栈顶开始弹出数据。栈具有记忆作用,对栈的插入和删除操作,不需要改变栈底的指针。
public class MyStack {
//这里栈的底层用数组来存储数据
int [] elements;
public MyStack() {
elements = new int[0];
}
//压入元素
public void push(int element) {
int[] newArr = new int[elements.length+1];
for(int i=0;i<elements.length;i++) {
newArr[i] = elements[i];
}
newArr[elements.length] = element;
elements = newArr;
}
//取出栈顶元素
public int pop() {
if(elements.length == 0) {
throw new RuntimeException("栈为空");
}else {
//取出数组最后一个元素
int element = elements[elements.length-1];
//创建一个新数组,长度为原数组-1
int [] newArr = new int [elements.length-1];
//将原数组中除了取出的元素的其他元素复制到新数组中
for(int i = 0;i<newArr.length;i++) {
newArr[i] = elements[i];
}
//替换数组
elements = newArr;
return element;
}
}
//查看栈顶元素
public int peek() {
if(elements.length == 0) {
throw new RuntimeException("栈为空");
}
return elements[elements.length-1];
}
//判断栈是否为空
public boolean isEmpty() {
return elements.length == 0;
}
}
3.3队列
队列是一种线性表的存储结构,和栈类似,它的操作受限,表现为先进先出,后进后出,队列有头指针和尾指针之分,删除元素从对头操作,添加元素从队尾操作。
public class MyQueue {
int[] elements;
public MyQueue() {
elements = new int[0];
}
//入队
public void add(int element) {
int[] newArr = new int[elements.length+1];
for(int i = 0;i<elements.length;i++) {
newArr[i] = elements[i];
}
newArr[elements.length] = element;
elements = newArr;
}
//出队
public int poll() {
//把数组中的第0个元素取出来
int element = elements[0];
//创建一个新的数组,长度为原来的数组-1
int [] newArr = new int[elements.length-1];
//赋值原数组的数据除了第0个元素,到新数组中
for(int i = 0;i<newArr.length;i++) {
newArr[i] = elements[i+1];
}
//赋值给原数组
elements = newArr;
return element;
}
//判断队列是否为空
public boolean isEmpty() {
return elements.length == 0;
}
3.4单链表
单链表:是链表中最简单的一种形式,它的每个节点包含两个域,一个信息域(表元素域)一个链接域,这个链接指向链表中的下一个节点,最后一个节点的链接域指向空值。
表元素域中用来存放具体的数据。
链接域中用于存放下一个节点的具体位置(指针)。
变量p指向单链表的头结点的位置,从p出发能找到链表中的任意节点。
//一个节点
public class Node {
//节点内容
int data;
//下一个节点
Node next;
public Node(int data) {
this.data = data;
}
//为节点追加节点
public Node append(Node node) {
/*追加方式为node1>node2,node2>node3
this.next = node;*/
//追加方式为node1>node2,node1>node3,
//当前节点
Node currentNode = this;
//循环向后找
while(true) {
//取出下一个节点
Node nextNode = currentNode.next;
//如果下一个节点是null,当前机电已经是最后一个节点了。
if(nextNode == null) {
break;
}
//将下一个节点赋值给当前节点
currentNode = nextNode;
}
//把需要追加的节点追加到当前找到的节点的下一个节点
currentNode.next = node;
return this;
}
//获取下一个节点
public Node next() {
return this.next;
}
//获取节点数据
public int getData() {
return this.data;
}
//判断当前节点是否是最后一个节点
public boolean isLast() {
return next == null;
}
//删除下一个节点
public void removeNext() {
//取到下下个节点
Node newNext = next.next;
//把下下个节点赋值为当前节点的下一个节点
this.next = newNext;
}
//显示所有节点信息
public void show() {
Node currentNode = this;
while(true) {
System.out.print(currentNode.data + " ");
currentNode = currentNode.next;
if(currentNode == null) {
break;
}
}
System.out.println();
}
//插入一个节点,作为当前节点的下一个节点
public void after(Node node) {
//取出下一个节点,作为下下个节点
Node nextNext = next;
//把新插入的节点作为当前接待的下一个节点
this.next = node;
//把原来的下下个节点设置为新节点的下一个节点
node.next = nextNext;
}
3.5循环链表
循环链表分为单向循环链表和双向循环链表。
单向循环链表:单向循环链表是单链表的另一种形式,其结构特点是链表中最后一个节点的指针不再是结束标记,而是指向整个链表的第一个节点,从而使单链表形成一个环。
和单链表相比,循环链表的长处是从链尾到链头比较方便,当要处理的数据元素具有环形结构特点时,适合采用循环单链表。
//一个节点
public class LoopNode {
//节点内容
int data;
//下一个节点
LoopNode next = this;
public LoopNode(int data) {
this.data = data;
}
//获取下一个节点
public LoopNode next() {
return this.next;
}
//获取节点数据
public int getData() {
return this.data;
}
//删除下一个节点
public void removeNext() {
//取到下下个节点
LoopNode newNext = next.next;
//把下下个节点赋值为当前节点的下一个节点
this.next = newNext;
}
//插入一个节点,作为当前节点的下一个节点
public void after(LoopNode node) {
//取出下一个节点,作为下下个节点
LoopNode nextNext = next;
//把新插入的节点作为当前接待的下一个节点
this.next = node;
//把原来的下下个节点设置为新节点的下一个节点
node.next = nextNext;
}
双向循环链表:在双向循环链表中,每个节点包括三个域,分别是element域,next域和prior域,其中element域为数据元素,next域为指向后继节点的对象引用,prior域为指向前驱结点的对象引用。
在双向链表中,有如下关系:设对象引用p表示双向链表中的第i个节点,则p.next表示第i+1个节点,p.next.prior仍然表示第i个节点,即p.next.prior == p;同样的,p.prior表示第i-1个节点,p.prior.next仍然表示第i个节点,即p.prior.next == p。
3.6递归
递归:在一个方法(函数)内部调用这个方法(函数)本身的编程方式。
斐波那契数列:菲波那切数列规定前两项是1,从第三项开始,每一项为相邻前两项的和。
1 1 2 3 5 8 13 21 34 …
public class TestFebonacci {
public static void main(String[] args) {
int i = febonacci(5);
System.out.println(i);
}
//打印第n项斐波那契数列
public static int febonacci(int i) {
if (i == 1 || i == 2) {
return 1;
}else {
return febonacci(i-1) + febonacci(i-2);
}
}
}
递归解决汉诺塔问题:
public class TestHanoi {
public static void main(String[] args) {
hanoi(3,'A','B','C');
}
/**
*
* @param n 盘子的数量
* @param from 开始的柱子
* @param in 中间的柱子
* @param to 目标柱子
*/
public static void hanoi(int n, char from, char in, char to) {
//只有 一个盘子
if (n == 1) {
System.out.println("把第"+n+"个盘子从"+from+"移动到"+to);
//无论有多少个盘子,都只认为有两个,上面所有的盘子和最下面一个盘子
}else {
//把上面所有的盘子移动到中间位置
hanoi(n-1,from,to,in);
//把最下面的一个盘子移动到目标位置
System.out.println("把第"+n+"个盘子从"+from+"移动到"+to);
//把中间位置上的盘子移动到目标位置
hanoi(n-1,in,from,to);
}
}
}
4.排序算法
4.1算法的时间复杂度和空间复杂度
时间复杂度简单来说就是这个算法执行需要多少时间,空间复杂度简单来说就是算法执行需要占用多少内存。
语句频度:在一个算法中,语句执行的次数称之为语句频度,T(n)表示。
时间复杂度:
一般情况下,算法中的基本操作语句的重复执行次数是问题规模n的某个函数,用T(n)表示,若有某个辅助函数f(n),使得当n趋近于无穷大时,T(n)/f(n)的极限值为不等于零的常数
,则称f(n)是T(n)的同数量级函数。记作T(n)=O(f(n)),称O(f(n))为算法的渐进时间复杂度,简称时间复杂度。
T(n)不同,但时间复杂度可能相同。如:T(n)=n2+5n+6与T(n)=3n2+3n+2它们的T(n)不同,但是时间复杂度相同,都是O(n2)。
常见的时间复杂度:
常数阶O(1)
对数阶O(log2n)
线性阶O(n)
线性对数阶O(nlog2n)
平方阶O(n2)
立方阶O(n3)
k次方阶O(nk)
指数阶O(2n)
随着问题规模n的不断增大,上述时间复杂度不断增大,算法的执行效率越低。
计算时间复杂度的方法:
用常数1代替运行时间中的所有加法常数,修改后的运行次数函数中,只保留最高阶项,去除最高阶项的系数。
4.2常用的排序算法
4.1冒泡排序
冒泡排序一次比较相邻的两个元素,如果顺序错误就把他们调换过来,知道没有元素需要交换,排序完成。
public class demo12 {
public static void main(String[] args) {
int [] arr = new int [] {7,2,5,6,1,8,10,3};
System.out.println(Arrays.toString(arr));
bubbleSort(arr);
System.out.println(Arrays.toString(arr));
}
public static void bubbleSort(int [] arr) {
//控制一共比较多少轮
for (int i = 0;i<arr.length-1;i++) {
//控制的是比较的次数
for (int j = 0;j < arr.length-1-i;j++) {
if (arr[j]>arr[j+1]) {
int temp = 0;
temp = arr[j];
arr[j] = arr[j+1];
arr[j+1] = temp;
}
}
}
}
}
4.2快速排序
快速排序是基于分治的思想,是冒泡排序的改进版。首先在数组中选取一个基准点,然后分别从数组的两端扫描数组。设两个指示标志,low指向起始的位置,high指向末尾的位置,首先从后半部分开始,如果发现有元素比该基准点的值小,就交换low和high位置的值,然后从前半部分开始扫描,发现有元素大于基准点的值,就交换low和high位置的值,如此往复循环,直到low>=high,然后把基准点的值放在high这个位置,一次排序完成,以后采用递归的方式分别对前半部分和后半部分排序,以达到整个序列有序的目的。
public static void main(String[] args) {
int[] arr = new int[] {6,4,3,8,9,1,2,7,5};
quickSort(arr, 0, arr.length-1);
System.out.println(Arrays.toString(arr));
}
public static void quickSort(int[] arr,int start,int end) {
if(start<end) {
//定义基准数
int stard = arr[start];
//设置两个指示标志
int low = start;
int high = end;
//循环找出比基准数大或者比基准数小的值
while(low < high) {
//右边的数字比基准数大,向左移动指示标志
while(low<high&&stard<=arr[high]) {
high--;
}
//右边的数字比基准数小,使用右边的数字替换左边的数字
arr[low] = arr[high];
//左边的数字比基准数小,向右移动指示标志
while(low < high&&arr[low]<=stard) {
low++;
}
//左边的数字比基准数字大,使用左边的数字替换右边的数字
arr[high] = arr[low];
}
arr[low] = stard;
//处理所有比基准数小的数字
quickSort(arr, start, low);
//处理所有比基准数大的数字
quickSort(arr, low+1, end);
}
}
4.3直接插入排序
直接插入排序是将排序数据分为已排序区间和未排序区间,依次遍历未排序区间,和已排序区间的值进行比较,将其插入到合适的位置上,直至将末排序列的区间数据遍历完。插入排序是很稳定的,最好情况下,数组本来就是有序的,所以我们只需要遍历一次数组就可以了,时间复杂度为O(n),最坏和平均的情况下,时间复杂度都是O(n2)。
public static void main(String[] args) {
int[] arr = new int[]{8,4,2,6,10,3,7};
insertSort(arr);
System.out.println(Arrays.toString(arr));
}
public static void insertSort(int[] arr) {
//遍历所有数字
for(int i = 1;i<arr.length;i++) {
//如果当前数字比前一个数字小
if(arr[i]<arr[i-1]) {
//定义一个临时变量
int temp = arr[i];
int j;
//遍历当前数字前面的数字,
for(j = i-1;j>=0&&temp<arr[j];j--) {
//满足前面数字大于临时变量的值,将前面一个数字的值赋值给后面一个
arr[j+1] = arr[j];
}
//将临时变量的值赋值给不满足条件的最后一个值
arr[j+1] = temp;
}
}
}
4.4希尔排序
希尔排序基于插入排序,并添加了一些新的特性,从而大大提高插入排序的执行效率。直接插入排序的缺陷,多次移动,假如一个一个很小的数据在靠右的位置上,那么要将该数据排序到正确的位置上,则所有的中间数据都需要向右移动一位。
希尔排序的基本思想,希尔排序是把数据按照下标的一定的增量分组,对每组使用直接插入排序算法排序,随着增量逐步减少,每组包含的关键词越来越多,当增量减至1时,整个文件恰被分成一组,算法终止。
public static void main(String[] args) {
int [] arr = new int[] {7,1,5,15,9,7,0,6,11};
shellSort(arr);
System.out.println(Arrays.toString(arr));
}
public static void shellSort(int[] arr) {
//遍历所有的步长
for(int d = arr.length/2;d>0;d=d/2) {
//遍历所有元素
for(int i = d;i<arr.length;i++) {
//遍历本组中的所有元素
for(int j = i - d;j >= 0;j-=d) {
//如果当前元素大于加上步长后的元素
if(arr[j] > arr[j+d]) {
int temp = arr[j];
arr[j] = arr[j+d];
arr[j+d] = temp;
}
}
}
}
}
4.5简单选择排序
选择排序是从冒泡排序演化而来的,每一轮比较出最小的那个值,放到第一的位置,然后在每轮的无序区中选出最小的值放在有序区最后的位置。
public static void main(String[] args) {
int[] arr = new int[]{9,5,1,3,5,7,2,4,9};
selectSort(arr);
System.out.println(Arrays.toString(arr));
}
//选择排序
public static void selectSort(int[] arr) {
//遍历所有元素
for(int i = 0;i<arr.length;i++) {
//定义一个下标
int minIndex = i;
//把当前遍历的数字和之后的所有数字进行比较,并记录下最小的数字的下标。
for(int j = i+1;j<arr.length;j++) {
//如果后面的数字比记录的数字还要小
if(arr[minIndex] > arr[j]) {
//记录下标
minIndex = j;
}
}
//如果最小的数和当前遍历的下标不一致,说明下标为minIndex的数字比当前遍历的数字更小
if(i != minIndex) {
int temp = arr[i];
arr[i] = arr[minIndex];
arr[minIndex] = temp;
}
}
}
4.6归并排序
归并排序是采用分治法的一个典型的应用,归并排序的思想就是先递归分解数组,再合并数组。不断的将大的数组分成两个小数组,直到不能拆分为止,即形成了单个值。此时使用合并的排序思想对已经有序的数组进行合并,合并为一个大的数据,不断重复此过程,直到最终所有数据合并到一个数组为止。
public static void main(String[] args) {
int[] arr = new int[] {1,3,5,2,4,6,8};
mergeSort(arr, 0, arr.length-1);
System.out.println(Arrays.toString(arr));
}
public static void mergeSort(int[] arr,int low,int high) {
int mid = (low+high)/2;
//当左边的下标不小于右边时结束递归
if(low<high) {
//处理左边
mergeSort(arr,low,mid);
//处理右边
mergeSort(arr,mid+1,high);
//归并操作
merge(arr,low,mid,high);
}
}
public static void merge(int[] arr,int low,int mid,int high) {
//定义一个临时数组,用于存储归并后的数组
int[] temp = new int[high-low+1];
//记录第一个数组中需要遍历的下标
int i = low;
//记录第二个数组中需要遍历的下标
int j = mid+1;
//用于记录在临时数组中的下标
int index = 0;
//遍历两个数组取出小的数字,放入临时数组中
while(i<=mid&&j<=high) {
//第一个数组的数据更小
if(arr[i]<=arr[j]) {
//把小的数据放入临时数组中
temp[index] = arr[i];
//把下标向后移一位
i++;
}else {
temp[index] = arr[j];
j++;
}
index++;
}
//处理多余的数据
while(j<=high) {
temp[index] = arr[j];
j++;
index++;
}
while(i<=mid) {
temp[index] = arr[i];
i++;
index++;
}
//把临时数组中的数据重新放入原数组中
for(int k = 0;k<temp.length;k++) {
arr[k+low] = temp[k];
}
}
4.7基数排序
基数排序思想是第一轮从个位上的数字进行分类,根据个位上的数字把元素放入对应的“桶”中,依次取出放入原数组中,第二轮排序从十位就行分类,不够补零,分别放入对应的“桶”中,第三轮排序从百位进行分类,不够补零,分别放入对应的“桶”中。以此类推,直到最大位数比较晚为止,即完成排序。
使用数组实现:
public static void main(String[] args) {
int[] arr = new int[]{79,563,1,259,36,49,2,15,89,798};
radixSort(arr);
System.out.println(Arrays.toString(arr));
}
public static void radixSort(int[] arr) {
//存数组中最大的那个数
int max = Integer.MIN_VALUE;
for (int i = 0;i<arr.length;i++) {
if(arr[i]>max) {
max = arr[i];
}
}
//计算最大数的位数
int maxLength = (max+"").length();
//用于临时存储数据的临时数组
int[][] temp = new int[10][arr.length];
//用于记录在相应数组中的数据个数
int[] counts = new int[10];
//根据最大数的位数决定比较次数
for(int k = 0,n=1;k<maxLength;k++,n*=10) {
//把每个数字分别计算余数
for(int j = 0;j<arr.length;j++) {
int ys = arr[j]/n%10;
//把当前遍历的元素放入到指定的数组中
temp[ys][counts[ys]] = arr[j];
//记录数量
counts[ys]++;
}
//记录取的元素需要放在原数组中的位置
int index = 0;
//把数字取出来
for(int a = 0;a<counts.length;a++) {
//记录数量的数组中不为零的才取出来
if(counts[a]!=0) {
//循环取元素
for(int b = 0;b<counts[a];b++) {
//取出元素
arr[index] = temp[a][b];
index++;
}
//将数量数组中的值归零
counts[a]= 0;
}
}
}
}
使用队列实现:
public static void main(String[] args) {
int[] arr = new int[]{79,563,1,259,36,49,2,15,89,798};
radixSort(arr);
System.out.println(Arrays.toString(arr));
}
public static void radixSort(int[] arr) {
//存数组中最大的那个数
int max = Integer.MIN_VALUE;
for (int i = 0;i<arr.length;i++) {
if(arr[i]>max) {
max = arr[i];
}
}
//计算最大数的位数
int maxLength = (max+"").length();
//用于临时存储数据的临时队列
MyQueue[] temp = new MyQueue[10];
//为队列数组赋值
for(int i = 0;i<temp.length;i++) {
temp[i] = new MyQueue();
}
//根据最大数的位数决定比较次数
for(int k = 0,n=1;k<maxLength;k++,n*=10) {
//把每个数字分别计算余数
for(int j = 0;j<arr.length;j++) {
int ys = arr[j]/n%10;
//把当前遍历的元素放入到指定的队列中
temp[ys].add(arr[j]);
}
//记录取的元素需要放在原数组中的位置
int index = 0;
//把数字取出来
for(int a = 0;a<temp.length;a++) {
//当前遍历的队列不为空才取
while(!temp[a].isEmpty()) {
//取出元素
arr[index] = temp[a].poll();
//记录下一个位置
index++;
}
}
}
}
4.8堆排序
升序排列使用大顶推;降序排列使用小顶堆。
堆排序的基本思想:将待排序的序列构成一个大顶堆;此时,整个序列的最大值就是对顶的根节点;将其与末尾元素进行交换,此时末尾就是最大值;然后将剩余n-1个元素重新构成一个堆,这样会得到n个元素的次小值,如此反复执行,便会得到一个有序序列。可以看出,在构建大顶堆的过程中,无序元素的个数逐步减少,最后就得到一个有序序列了。
public static void main(String[] args) {
int[] arr = new int[]{9,6,8,7,0,1,10,4,2};
heapSort(arr);
System.out.println(Arrays.toString(arr));
}
public static void heapSort(int[] arr) {
//开始位置:开始节点为最后一个非叶子节点,即是最后一个叶子节点的父节点
int start = (arr.length-1)/2;
//循环调整,将数组调整为大顶堆
for(int i = start;i>=0;i--) {
maxHeap(arr, arr.length, i);
}
//先把第0个元素和堆中最后一个元素交换位置,在把前面的处理为大顶堆
for(int j = arr.length-1;j>0;j--) {
int temp = arr[0];
arr[0] = arr[j];
arr[j] = temp;
maxHeap(arr, j, 0);
}
}
public static void maxHeap(int[] arr,int size,int index) {
//左子节点
int leftNode = 2*index+1;
//右子节点
int rightNode = 2*index+2;
int max = index;
//和两个子节点对比,找到最大的节点
if(leftNode<size&&arr[leftNode]>arr[max]) {
max = leftNode;
}
if(rightNode<size&&arr[rightNode]>arr[max]) {
max = rightNode;
}
//交换位置
if(max != index) {
int temp = arr[index];
arr[index] = arr[max];
arr[max] = temp;
//调换位置后需要重新判断子节点是否有更大的值,递归实现
maxHeap(arr, size, max);
}
}
5.树结构
5.1树结构概述
5.2二叉树
二叉树:对于一个树,任何一个子节点的子节点数量都不超过2。
二叉树的子节点分为左节点和右节点。
满二叉树:所有叶子节点都在最后一层,而且节点的总数为2n-1,n是树的高度。
完全二叉树:所有叶子节点都在最后一层或者倒数第二层,且最后一层的叶子结点在左边连续,倒数第二层的叶子结点在右边连续。满二叉树也是完全二叉树。
链式存储的二叉树:
public class BinaryTree {
TreeNode root;
//设置根节点
public void setRoot(TreeNode root) {
this.root = root;
}
//获取根节点
public TreeNode getRoot() {
return root;
}
public class TreeNode {
//节点的权
int value;
//左节点
TreeNode lNode;
//右节点
TreeNode rNode;
public TreeNode(int value) {
this.value = value;
}
//设置左节点
public void setlNode(TreeNode lNode) {
this.lNode = lNode;
}
//设置右节点
public void setrNode(TreeNode rNode) {
this.rNode = rNode;
}
public class TestBinaryTree {
public static void main(String[] args) {
//创建一棵树
BinaryTree binTree = new BinaryTree();
//创建一个根节点
TreeNode root = new TreeNode(1);
//添加根节点
binTree.setRoot(root);
//创建两个节点
TreeNode node2 = new TreeNode(2);
TreeNode node3 = new TreeNode(3);
root.setlNode(node2);
root.setrNode(node3);
}
顺序存储的二叉树
顺序存储的二叉树通常情况只考虑完全二叉树。
顺序存储的二叉树的特性:
第n个元素的左子节点是:2n+1
第n个元素的右子节点是:2n+2
第n个元素的父节点是(取整数):(n-1)/2
遍历顺序存储的二叉树
int[] data;
public ArrayBinaryTree(int[] data) {
this.data = data;
}
public void frontShow() {
frontShow(0);
}
public void frontShow(int index) {
if(data == null || data.length == 0) {
return;
}
//先遍历当前节点的内容
System.out.println(data[index]);
//遍历左子节点,第n个元素的左子节点是:2*n+1
if(2*index+1 < data.length) {
frontShow(2*index+1);
}
//遍历右子节点,第n个元素的右子节点是:2*n+2
if(2*index+2 < data.length) {
frontShow(2*index+2);
}
}