文章目录
一、排序
1. 交换排序
1.1 冒泡排序
- 基本思想:两个数比较大小,较大的数下沉,较小的数会经过交换冒起来。
- 过程:
- 双层循环,第一层控制比较次数(比如说只找出k个最小的数时,第一层循环只需k次);第二层循环从后向前(或从前往后)两两比较;
- 比如升序排序,如果第二个数小就交换位置。最终最小数被交换到起始的位置,这样第一个最小数的位置就排好了。
- 继续重复上述过程,依次将第2.3…n-1个最小数排好位置。
- 动图演示:
- 代码实现:
public void BubboSort(int[] nums){
int temp;//临时变量
Boolean flag;//是否交换的标志
for (int i = 0; i < nums.length-1; i++) {//表示趟数,一共 arr.length-1 次。
//若仅需输出k个最小的数时,可修改为i<k❗️❗️(第二层循环需从后向前遍历)
//若仅需输出k个最大的数时,可修改为i<k❗️❗️(第二层循环需从前向后遍历)
// 每次遍历标志位都要先置为false,才能判断后面的元素是否发生了交换
flag = false;
//让最小的数依次排在前面
for (int j = arr.length-1; j > 0; j--) {
if (arr[j] < arr[j-1]){
temp = nums[j];
nums[j] = nums[j-1];
nums[j-1] = temp;
flag = true;//只要有发生了交换,flag就置为true
}
}
if (!flag)break;
}
}
1.2 快速排序
- 算法思想:(分治)
- 先从数列中取出一个数作为“基准”(pivot);
- 将比基准小的数全部放在它的左边,大于或等于基准的数放在它的右边;在这个分区退出之后,该基准就处于数列的中间位置。这个称为分区(partition)操作;
- 两部分数据依次递归排序下去,直至各区间只有1个数。
- 动图演示:
- 代码实现:
//自己写的
public void QuickSort(int[] nums,int l,int r){
if(l>=r)return;
int i=l,j=r;
int pivot = nums[l];//选择第一个数为pivot
while (i<j){
while (i<j && nums[j]>=pivot)j--;//从右向左找第一个小于pivot的数
nums[i] = nums[j];
while ((i<j && nums[i]<pivot))i++;//从左向右找第一个大于pivot的数
nums[j] = nums[i];
}
nums[i] = pivot;
QuickSort(nums,l,i);//递归调用
QuickSort(nums,i+1,r);//递归调用
}
2. 选择排序
2.1 直接选择排序
- 工作原理:
首先在未排序序列中找到最小(大)元素,存放到排序序列的起始位置,然后,再从剩余未排序元素中继续寻找最小(大)元素,然后放到已排序序列的末尾。以此类推,直到所有元素均排序完毕。 - 过程:
- 在长度为N的无序数组中,第一次遍历n-1个数,找到最小的数值与第一个元素交换;
- 第二次遍历n-2个数,找到最小的数值与第二个元素交换;
- …
- 第n-1次遍历,找到最小的数值与第n-1个元素交换,排序完成。
- 动图演示:
- 代码:
public void SelectionSort(int[] nums){
int mini;
int temp;
for (int i = 0; i < nums.length; i++) {//若仅需找出k个最小的数,则可改为i<k❗️❗️
mini = i;
for (int j = i+1;j<nums.length;j++){
if (nums[j]<nums[mini]){
mini = j;
}
}
if (mini!=i){
temp = nums[mini];
nums[mini] = nums[i];
nums[i] = temp;
}
}
}
2.2 堆排序
链接: 堆排序
堆排序(Heapsort)是指利用堆这种数据结构所设计的一种排序算法。堆积是一个近似完全二叉树的结构,并同时满足堆积的性质:即子结点的键值或索引总是小于(或者大于)它的父节点。
- 基本思想:
利用大顶堆(小顶堆)堆顶记录的是最大关键字(最小关键字)这一特性,使得每次从无序中选择最大记录(最小记录)变得简单。- ① 将待排序的序列构造成一个最大堆,此时序列的最大值为根节点;
- ② 依次将根节点与待排序序列的最后一个元素交换;
- ③ 再维护从根节点到该元素的前一个节点为最大堆,如此往复,最终得到一个递增序列
- 实现逻辑:
- ① 先将初始的R[0…n-1]建立成最大堆,此时是无序堆,而堆顶是最大元素。
- ② 再将堆顶R[0]和无序区的最后一个记录R[n-1]交换,由此得到新的无序区R[0…n-2]和有序区R[n-1],且满足R[0…n-2].keys ≤ R[n-1].key
- ③ 由于交换后新的根R[1]可能违反堆性质,故应将当前无序区R[1…n-1]调整为堆。然后再次将R[1…n-1]中关键字最大的记录R[1]和该区间的最后一个记录R[n-1]交换,由此得到新的无序区R[1…n-2]和有序区R[n-1…n],且仍满足关系R[1…n-2].keys≤R[n-1…n].keys,同样要将R[1…n-2]调整为堆。
- ④ 直到无序区只有一个元素为止。
- 链接: 堆排序算法动图演示
- 堆排序可以用到上一次的排序结果,所以不像其他一般的排序方法一样,每次都要进行n-1次的比较,复杂度为O(nlogn)
- 优点:O(nlogn),原地排序。最大特点:每个节点的值>=或<=其子树节点
- 缺点:相比于快排,数据访问方式没有快排好;数据的交换次数多于快排。
- 平均时间复杂度:O(NlogN)
由于每次重新恢复堆的时间复杂度为O(logN),共N - 1次重新恢复堆操作,再加上前面建立堆时N / 2次向下调整,每次调整时间复杂度也为O(logN)。二次操作时间相加还是O(N * logN)。
import java.util.Arrays;
public class HeapSort {
/**小根堆
* 堆排序的主要入口方法,共两步。
*/
public void sort(int[] arr){
/*
* 第一步:建立初始堆
* 从第一个非叶子节点开始......(beginIndex = 第一个非叶子节点)
* 从第一个非叶子节点开始即可。无需从最后一个叶子节点开始。 叶子节点可以看作已符合堆要求的节点,根节点就是它自己且自己以下值为最大。
*/
int r = arr.length - 1;
int beginIndex = (r - 1) / 2;//第一个非叶子节点
for(int i = beginIndex; i >= 0; i--){
maxHeapify(arr,i, r);//
}
/*
* 第二步:对堆化数据排序
* 每次都是移出最顶层的根节点A[0],与最尾部节点位置调换,同时遍历长度 - 1。
* 然后从新整理被换到根节点的末尾元素,使其符合堆的特性。
* 直至未排序的堆长度为 0。
*/
for (int i = 0; i < r; i++) {
swap(arr,0,r-i);
maxHeapify(arr,0,r-i-1);
}
// for (int i = r;i>0;i--){
// swap(arr,0,i);
// maxHeapify(arr,0,i-1);
// }
}
/** 调整堆
* 调整索引为 index 处的数据,使其符合堆的特性。
*
* @param index 需要堆化处理的数据的索引
* @param r 未排序的堆(数组)的长度
*/
private void maxHeapify(int[] arr,int index,int r){
int li = 2 * index + 1; // 左子节点索引
int ri = li + 1; // 右子节点索引
int cMax = li; // 子节点值最大索引,默认左子节点。
if (li>r){
return;
}
if (ri<=r && arr[ri]>arr[li]){
cMax = ri;
}
if (arr[index]< arr[cMax]){
swap(arr,index,cMax); // 如果父节点被子节点调换,
maxHeapify(arr,cMax,r); // 则需要继续判断换下后的父节点是否符合堆的特性。
}
}
private void swap(int[] arr,int i,int j){
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
public static void main(String[] args) {
int[] arr = new int[]{3,5,3,0,8,6,1,5,8,6,2,4,9,4,7,0,1,8,9,7,3,1,2,5,9,7,4,0,2,6};
new HeapSort().sort(arr);
System.out.println(Arrays.toString(arr));
}
}
Java小根堆、大根堆?
- 小根堆:
PriorityQueue<Integer> queue = new PriorityQueue<>();
queue.offer();//入队
queue.poll();//升序出队
- 大根堆:
PriorityQueue<Integer> queue = new PriorityQueue<>((o1, o2)->o2.compareTo(o1));
queue.offer();//入队
queue.poll();//降序出队
3. 插入排序
3.1 直接插入排序
- 算法思想:在要排序的一组数中,假定前n-1个数已经排好序,现在将第n个数通过交换插到前面的有序数列中,使得这n个数也是排好顺序的。如此反复循环,直到全部排好顺序。
- 代码实现:
public void InsertSort(int[] nums){
int temp;
for (int i = 1; i < nums.length; i++) {
for (int j = i;j>0;j--){
if (nums[j]>nums[j-1]){
//不需要交换
break;
}else {
temp = nums[j];
nums[j] = nums[j-1];
nums[j-1] = temp;
}
}
}
}
3.2 折半插入排序
3.3 Shell排序
4. 归并排序
- 稳定的排序算法,任何情况下时间复杂度都是O(nlogn)
- 归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。
- 基本思路:
- 就是将数组分成2组A,B,如果这2组组内的数据都是有序的,那么就可以很方便的将这2组数据进行排序。如何让这2组组内数据有序?
- 可以将A,B组各自再分成2组。依次类推,当分出来的小组只有1个数据时,可以认为这个小组组内已经达到了有序,然后再合并相邻的2个小组就可以了。这样通过先递归的分解数列,再合并数列就完成了归并排序。
- 就是将数组分成2组A,B,如果这2组组内的数据都是有序的,那么就可以很方便的将这2组数据进行排序。如何让这2组组内数据有序?
- (先递归地将数组分组,直到分出来的组只有一个数据时,就可以认为这个组已经达到了有序, 再进行合并。)
- 过程:
- 代码:
//合并 :将两个序列a[l-mid],a[mid+1-r]合并
public void mergeArray(int[] nums,int l, int mid, int r,int[] temp){
int i = l,j=mid+1,k=0;
while(i<=mid && j<=r){
if(nums[i]<=nums[j]){
temp[k++] = nums[i++];
}else{
temp[k++] = nums[j++];
}
}
while(i<=mid){
temp[k++] = nums[i++];
}
while(j<=r){
temp[k++] = nums[j++];
}
for(int m = 0;m<k;m++){//将temp中的元素 全部都copy到原数组里边去
nums[m+l] = temp[m];
}
}
public void mergeSort(int[] nums,int l,int r,int[] temp){
if(l<r){
int mid = (l+r)/2;
// 分成两部分进行排序
mergeSort(nums,l,mid,temp);
mergeSort(nums,mid+1,r,temp);
// 将两部分进行合并
mergeArray(nums,l,mid,r,temp);
}
}
5. 桶排序
- 工作原理:是把数据分到桶里,每个桶再分别排序(有可能再使用别的排序算法或是以递归方式继续使用桶排序进行排序),最后依次把各个桶中的记录列出来记得到有序序列。
- 它利用了函数的映射关系,高效与否的关键就在于这个映射函数的确定。为了使桶排序更加高效,我们需要做到这两点:
- 1)在额外空间充足的情况下,尽量增大桶的数量;
- 2)使用的映射函数能够将输入的 N 个数据均匀的分配到 K 个桶中。
- 算法步骤
(设置一个 bucketSize,作为每个桶所能放置多少个不同数值;
动态数组ArrayList
作为桶,桶里放的元素也用 ArrayList 存储;桶的数量为:(max-min)/bucketSize+1
)- 1)遍历数组,把数据映射到对应的桶中。映射函数:
(nums[i]-min)/bucketSize
- 2)对每个非空的桶进行排序。(可以使用
Collections.sort(...)
方法,可以使用其他排序方法,也可以递归使用桶排序) - 3)遍历桶数组,把排序好的数据放进目标数组中。
- 1)遍历数组,把数据映射到对应的桶中。映射函数:
public static void bucketSort(int[] arr){
int max = Integer.MIN_VALUE;
int min = Integer.MAX_VALUE;
for(int i = 0; i < arr.length; i++){
max = Math.max(max, arr[i]);
min = Math.min(min, arr[i]);
}
//桶数
int bucketNum = (max - min) / arr.length + 1;
ArrayList<ArrayList<Integer>> bucketArr = new ArrayList<>(bucketNum);
for(int i = 0; i < bucketNum; i++){
bucketArr.add(new ArrayList<Integer>());
}
//将每个元素放入桶
for(int i = 0; i < arr.length; i++){
int num = (arr[i] - min) / (arr.length);
bucketArr.get(num).add(arr[i]);
}
//对每个桶进行排序
for(int i = 0; i < bucketArr.size(); i++){
Collections.sort(bucketArr.get(i));
}
System.out.println(bucketArr.toString());
}
6.基数排序
二、查找
折半查找 (二分查找)
- 基本思路:
- 首先将给定值
key
与表中 中间位置元素的关键字比较 - 若相等则查找成功,返回该元素的存储位置;
- 若不等,则所需查找的位置只能在中间元素以外的前半部分或后半部分。
- 然后在缩小的范围内 继续进行同样的查找,如此重复,直到找到为止,或确定表中没有所需要查找的元素,找不到则返回查找失败的信息。
- 首先将给定值
- 模版:
int binarySearch(int[] arr , Integer key){
int low = 0,high = arr.length-1;
int mid = (low+high)/2;
while (low<=high){
if (arr[mid]==key){
return mid;
}else if (arr[mid]>key){
high = mid-1;
}else {
low = mid+1;
}
mid = (low+high)/2;
}
return -1;
}
三、树的遍历
链接: 有关二叉树,对称性递归
树节点:
class TreeNode {
int val;
//左子树
TreeNode left;
//右子树
TreeNode right;
//构造方法
TreeNode(int x) {
val = x;
}
}
先序遍历 NLR
- 递归先序遍历
递归先序遍历很容易理解,先输出节点的值,再递归遍历左右子树。中序和后序的递归类似,改变根节点输出位置即可。
// 递归先序遍历
public static void recursionPreorderTraversal(TreeNode root) {
if (root != null) {
System.out.print(root.val + " ");
recursionPreorderTraversal(root.left);
recursionPreorderTraversal(root.right);
}
}
- 非递归先序遍历
因为要在遍历完节点的左子树后接着遍历节点的右子树,为了能找到该节点,需要使用栈来进行暂存。中序和后序也都涉及到回溯,所以都需要用到栈。
// 非递归先序遍历
public static void preorderTraversal(TreeNode root) {
// 用来暂存节点的栈
Stack<TreeNode> treeNodeStack = new Stack<TreeNode>();
// 新建一个游标节点为根节点
TreeNode node = root;
// 当遍历到最后一个节点的时候,无论它的左右子树都为空,并且栈也为空
// 所以,只要不同时满足这两点,都需要进入循环
while (node != null || !treeNodeStack.isEmpty()) {
// 若当前考查节点非空,则输出该节点的值
// 由考查顺序得知,需要一直往左走
while (node != null) {
System.out.print(node.val + " ");
// 为了之后能找到该节点的右子树,暂存该节点
treeNodeStack.push(node);
node = node.left;
}
// 一直到左子树为空,则开始考虑右子树
// 如果栈已空,就不需要再考虑
// 弹出栈顶元素,将游标等于该节点的右子树
if (!treeNodeStack.isEmpty()) {
node = treeNodeStack.pop();
node = node.right;
}
}
}
二叉树如何求深度?
- 方法一:递归
- 递归计算左子树的深度计为m;
- 递归计算右子树的深度计为n;
- 如果m大于n,二叉树的深度为m+1,否则为n+1;
- 方法二:广度优先遍历
- 方法一:递归
class TreeNode{
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) { val = x; }
}
//递归
public int maxDepth(TreeNode root) {
if(root==null) return 0;
return 1+Math.max(maxDepth(root.left), maxDepth(root.right));//返回左子树深度与右子树深度+1的最大值
}
- 方法二:广度优先遍历(利用队列)
class TreeNode{
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) { val = x; }
}
public int maxDepth(TreeNode root) {
if(root==null) return 0;
int ans = 0;//用来记录第几层
Queue<TreeNode> queue = new LinkedList<TreeNode>();//利用队列
queue.add(root);//将根节点加入队列
while(!queue.isEmpty()) {
int size = queue.size();
while(size>0) {//while循环负责:①将本层的节点出队列;②将本层节点的左节点即右节点加入队列
TreeNode node = queue.poll();
if(node.left!=null) {
queue.add(node.left);
}
if(node.right!=null){
queue.add(node.right);
}
size--;
}
ans++;
}
return ans;
}
树的层次遍历?
- 利用队列
- 核心思想:每次出队一个元素,就将该元素的孩子节点加入队列中,直至队列中元素个数为0时,出队的顺序就是该二叉树的层次遍历结果。
public class MyTree {
public static class TreeNode{
public Object key;
public TreeNode parent;
public List<TreeNode> childrens = new ArrayList<>();
public TreeNode(Object key) {
this.key = key;
}
}
private int size = 0;
private TreeNode root;
public MyTree(TreeNode root) {
this.root = root;
size++;
}
// 层次遍历
public List<TreeNode> levelOrder(TreeNode treeNode) {
/**
* 层次遍历使用到了广度优先搜索,技巧:深度优先用递归,广度优先用队列。
*/
Queue<TreeNode> queue = new LinkedList<>();
List<TreeNode> list = new LinkedList<>();
queue.add(treeNode);
while (queue.size()>0){
//出一个,进n个
//出一个
TreeNode node = queue.poll();
list.add(node);
//进n个
List<TreeNode> childens = node.childrens;
for (TreeNode childNode : childens) {
queue.add(childNode);
}
}
return list;
}
}
知识点
Arrays.sort排序、Collection.sort排序
Collection.sort是给List进行排序,而Arrays.sort是给数组进行排序。
Arrays.sort排序
Collection.sort排序
Collections.sort(list,Comparator<T>);
集合类型转为其他类型
- 怎样将
Set<String>
类型转为String[]
数组类型?集合.toArray(new String[0]);
- 怎样将集合类型转为
String
类型:StringUtils.join(集合,"");
- ArrayList转为int[]类型:
集合名.stream().mapToInt(Integer::valueOf).toArray();
- 将
ArrayList<int[]>
类型转为int[][]
类型:集合.toArray(new int[0][0]);
遍历Map:
for (Map.Entry<String, String> entry : 集合名字.entrySet()) {
String mapKey = entry.getKey();//key
String mapValue = entry.getValue();//value
}