头指针是指链表指向第一个结点的指针,头指针具有标识作用,常用头指针冠以链表的名字,不论链表是否为空头指针均不为空!
单链表的读取:在单链表中第i个元素没法一开始就知道,必须得从头开始找,复杂度O(n);由于单链表结构中没有定义表长因此不能用for来控制循环,用while + “工作指针后移:p=p.next”。
单链表的插入和删除:
插入:s.next=p.next; p.next=s;//顺序不能反,否则会断链
删除:p.next=p.next.next;
分为两部分:第一部分就是遍历查找第i个元素,复杂度O(n);第二部分就是插入删除操作,复杂度O(1);对于插入或者删除数据越频繁的操作,单链表的效率优势越明显(一个位置插入多个数据情况)
单链表的整表创建:头插法+尾插法
public static ListNode CreateListHead(int n){//n为新建链表的长度
ListNode head=new ListNode(-1);
//头插法
for (int i = 0; i < n; i++) {
ListNode node = new ListNode(i);
node.next=head.next;
head.next=node;
}
return head.next;
}
public static ListNode CreateListHead1(int n){
//尾插法
ListNode head = new ListNode(-1);
ListNode p=head;
for (int i = 0; i < n; i++) {
ListNode node = new ListNode(i*2);
p.next=node;
p=node;
}
return head.next;
}
/*用数组实现栈,并实现压入弹出方法*/
public static int top=-1;//栈顶指针
public static boolean push(int[] stack,int value){
top++;//语句顺序不能反
stack[top]=value;
return true;
}
public static int pop(int[] stack){
int res=stack[top];
top--;
return res;
}
public static int top1=-1;//栈1的栈顶指针
public static int top2=20;//栈2的栈顶指针
public static boolean flag1=false;//栈1进栈标志位
public static boolean flag2=false;//栈2进栈标志位
public static boolean push(int[] stack,int value){
if (top1+1==top2) return false;
if (flag1==true){
stack[++top1]=value;
}else if(flag2==true){
stack[--top2]=value;
}
return true;
}
public static int pop(int[] stack){
if (flag1==true){
if (top1==-1) return -1;
int temp=stack[top1];
stack[top1]=0;//弹出后将该位置元素清空
top1--;
return temp;
}else if (flag2==true){
if (top2==stack.length) return -1;
int temp=stack[top2];
stack[top2]=0;//弹出后将该位置元素清空
top2--;
return temp;
}else return -1;
}
public static int top=-1;//栈顶指针
public static int count=0;//栈的长度
public static void push(ListNode list,int value){
ListNode node=new ListNode(value);
node.next=top;
top=node;
count++;
}
public static int pop(ListNode list){
int temp=top.val;
top=top.next;
count--;
return temp;
}
//递归方法
Fbi(int n){
if(n<2) return n==0?0:1;
return Fbi(n-1)+Fbi(n-2);
}
//迭代方法
Fbi(int n){
int[] a=new int[n+1];
a[0]=0;
a[1]=1;
for(int i=2;i<n+1;i++){
a[i]=a[i-1]+a[i-2];
}
return a[n];
}
串是由零个或多个字符组成的有限序列,又名字符串
串的比较:比较的是字符的ASCII码值(两个规则,如下)
1.当s=“hap”,t=“happy”,则s
ASCII码值记忆:字符及其对应的ASCII码值
0~9:48 ~57(十进制)
A~Z:65 ~90
a~z:97 ~122
子串的定位操作通常称为串的模式匹配,应该算是串中最重要的操作之一!
朴素的模式匹配算法:就是对主串的每一个字符作为子串开头,与要匹配的字符串进行匹配。对主串做大循环,每个字符开头做T的长度的小循环,直到匹配成功或全部遍历完成为止
public static int Index(String str1,String str2,int pos){
char[] char1=str1.toCharArray();
char[] char2=str2.toCharArray();
int i=pos;//主串当前位置下标
int j=0;//子串当前位置下标
while (i<char1.length-char2.length+1 && j<char2.length){//减少循环次数,剩余长度没有子串长时不再遍历
if (char1[i]==char2[j]){
i++;
j++;
}else{
i=i-j+1;//退回到上次匹配首位的下一位
j=0;
}
}
if (j>=char2.length) return i-char2.length;//匹配成功
else return -1;//匹配失败
}
二叉树(binary tree)每个结点最多有两颗子树,左子树和右子树是有顺序的,要区分开!
特殊二叉树:
斜树:所有结点都只有左子树(右子树)的二叉树叫左斜树(右斜树)
满二叉树:所有分支结点都存在左子树和右子树,并且所有叶子都在同一层上
完全二叉树:对一棵具有n个结点的二叉树按层序编号,如果编号为i(i<=n)的结点与同样深度的满二叉树中编号为i的结点在二叉树中位置完全相同,则为完全二叉树
二叉树的性质
1.在二叉树的第 i 层上至多有 2i-1 个结点(i>=1)
2.深度为k的二叉树至多有2k-1个结点(k>=1),即等比数列ak=a12k-1的和
3.对任何一棵二叉树,如果其终端结点(叶子结点)数为n0,度为2的结点数为n2,则n0=n2+1,用分支总数联立结点总数求解
4.具有n个结点的完全二叉树的深度为[log2n]+1,([x]表示不大于x的最大整数)
5.层序遍历二叉树(n个结点):若i>1,则其双亲是结点i/2 ;若2i>n,则结点i无左孩子,否则其左孩子是结点2i ;若2i+1>n,则结点i无右孩子,否则其右孩子是结点2i+1
遍历二叉树:要求是每个结点被访问一次且仅被访问一次
前序遍历(先根再左再右)、中序遍历(先左再根再右)、后序遍历(先左再右再根)
public class TreeNode {//二叉树结点表示形式
int val;
TreeNode left;
TreeNode right;
public TreeNode(int val){
this.val=val;
}
}
public static void PreTraverse(TreeNode root, ArrayList<Integer> list) {
list.add(root.val);//前序遍历递归实现,并按前序遍历顺序将结点值存入list
if (root.left != null) PreTraverse(root.left, list);
if (root.right != null) PreTraverse(root.right, list);
}
public static void MidTraverse(TreeNode root, ArrayList<Integer> list) {
if (root.left != null) MidTraverse(root.left, list);
list.add(root.val);//中序遍历递归实现
if (root.right != null) MidTraverse(root.right, list);
}
public static void PostTraverse(TreeNode root, ArrayList<Integer> list) {
if (root.left != null) PostTraverse(root.left, list);
if (root.right != null) PostTraverse(root.right, list);
list.add(root.val);//后序遍历递归实现
}
/*迭代实现二叉树前序遍历*/
static void PreTraverse1(TreeNode root, ArrayList<Integer> list) {
if (root == null) return;
Stack<TreeNode> stack = new Stack<>();
//方法一:
/*stack.push(root);
while (!stack.isEmpty()) {
root=stack.pop();
list.add(root.val);
if (root.right != null) stack.push(root.right);
if (root.left != null) stack.push(root.left);
}*/
//方法二:
while (!stack.isEmpty() || root != null) {
while (root != null) {
list.add(root.val);//先访问再入栈
stack.push(root);
root = root.left;
}
root = stack.pop();//如果是null,出栈并处理右子树
root = root.right;
}
}
/*迭代实现二叉树中序遍历*/
static void MidTraverse1(TreeNode root, ArrayList<Integer> list) {
if (root == null) return;
Stack<TreeNode> stack = new Stack<>();
while (root != null || stack.isEmpty()) {
while (root != null) {
stack.push(root);
root = root.left;
}
root=stack.pop();
list.add(root.val);
root = root.right;
}
}
/*迭代实现二叉树后序遍历*/
static void PostTraverse1(TreeNode root, ArrayList<Integer> list) {
if (root == null) return;
Stack<TreeNode> stack = new Stack<>();
stack.push(root);
while (!stack.isEmpty()) {
list.add(stack.pop().val);//linkedlist采用addFirst方法,这样就可不用反转list了
if (root.left != null) stack.push(root.left);
if (root.right != null) stack.push(root.right);
}
Collections.reverse(list);
}
/*递归实现二叉树层序遍历*/
static void LevelTraverse(TreeNode root, ArrayList<Integer> list) {
if (root == null) return;
LinkedList<TreeNode> Queue = new LinkedList<>();//创建一个队列,用队列实现层序遍历
Queue.addLast(root);
while (!Queue.isEmpty()) {
TreeNode temp= Queue.removeFirst();
list.add(temp.val);
if (temp.left != null) Queue.addLast(temp.left);
if (temp.right != null) Queue.add(temp.right);
}
}
public class CreateBinaryTree {
/*构造一棵二叉树*/
static int index = 0;//此值标记传入数组下标,不可缺
/*以前序遍历的方法构造*/
static TreeNode Init(int[] array, int i) {
//一个用于构造二叉树的数组(前序遍历,0元素表示空结点);数组下标
if (array[index] == 0) {
return null;
}
TreeNode node = new TreeNode(array[index]);
node.left = Init(array, ++index);
node.right = Init(array, ++index);
return node;
}
static void PreTraverse(TreeNode root, ArrayList<Integer> list) {
list.add(root.val);//前序遍历递归实现,并按前序遍历顺序将结点值存入list
if (root.left != null) PreTraverse(root.left, list);
if (root.right != null) PreTraverse(root.right, list);
}
public static void main(String[] args) {
TreeNode node = new TreeNode();
int[] arr = {1, 2, 0, 0, 3, 0, 0};
TreeNode root = CreateBinaryTree.Init(arr, 0);
ArrayList<Integer> list = new ArrayList<>();
PreTraverse(root,list);
for (int i = 0; i < list.size(); i++) {
System.out.print(list.get(i)+" ");
}
}
}
查找(Searching)就是根据给定的某个值,在查找表中确定一个其关键字等于给定值的数据元素(或记录)
查找概论:
查找表(Search Table)是由同一类型的数据元素(或记录)构成的集合
关键字(key)是数据元素中某个数据项的值,又称键值,可用他标识一个数据元素
若此关键字可以唯一的标识一个记录,则称为主关键字,对那些可以标识多个数据元素的关键字,称为次关键字,主关键字所在的数据项称为主关键码
静态查找表:只做查找操作的查找表,主要操作有1、查询某个数据元素是否在查找表中 2、检索某个数据元素的各种属性(用线性表结构来组织数据)
动态查找表:在查找过程中同时插入查找表中不存在的数据元素,或者从查找表中删除已经存在的某个数据元素,主要操作有1、查找时插入数据元素 2、查找时删除数据元素(可考虑用二叉排序树的查找技术)
//相比于for循环,此优化算法更有优势,省去了每次都要判断i是否越界的语句
public int SequentialSearch(int[] arr,int key){
int fast=arr[0];//暂存arr[0]
int i=arr.length-1;
arr[0]=key;//作为哨兵
while(arr[i]!=key) i--;
arr[0]=fast;
if(i>0 || fast==key) return i;
return -1;//查找失败,表中无key数据元素
}
//折半查找的复杂度为O(logn),好过顺序查找的O(n)
Arrays.sort(arr);//传入BinarySearch的数组必须是有序的,这是前提
public int binarySearch(int[] arr,int key){
int low=0;//数组最左边
int high=arr.length-1;//数组最右边
int mid;
while(low<=high){
mid=(low+high)/2;//计算数组的中间位置
//mid=low+(high-low)*(key-arr[low])/(arr[high]-arr[low]);//插值查找,只需在折半查找的代码中改这一行代码
if(key<arr[mid]) high=mid-1;
else if(key>arr[mid]) low=mid+1;
else return mid;
}
return -1;//查找失败,表中无key元素
}
//递归实现
static int binarySearch(int[] arr, int left, int right, int target) {
if (left > right) return -1;
int mid = (left + right) / 2;
if (arr[mid] == target)
return mid;
else if (arr[mid] < target) {
return binarySearch(arr, mid + 1, right, target);
} else {
return binarySearch(arr, left, mid - 1, target);
}
}
直接通过关键字key得到要查找的记录内存存储位置
存储位置 = f (关键字)
通过此函数公式,我们可以通过查找关键字不需要比较就可获得需要的记录的存储位置,这就是一种新的存储技术——散列技术
散列技术是在记录的存储位置和它的关键字之间建立一个确定的对应关系f,使得每个关键字key对应一个存储位置f(key)。我们把这种对应关系f称之为散列函数,又称哈希(hash)函数
按以上思想,采用散列技术将记录(数据元素)存储在一块连续的存储空间中,这块连续存储空间称为散列表或哈希表(hash table),那么关键字对应的记录存储位置我们称之为散列地址!
散列表查找步骤
整个散列过程分为两步:
1、在存储时,通过散列函数计算记录的散列地址,并按此散列地址存储该记录
2、当查找记录时,我们通过同样的散列函数计算记录的散列地址,按此散列地址访问该记录
冲突:两个关键字(key1!=key2),但是却有f(key1) =f(key2),这种现象我们称之为冲突,并把key1和key2称为这个散列函数的同一词
散列函数构造方法
直接定址法:f(key)=a*key+b
数字分析法:使用关键字的一部分来计算散列存储位置
平方取中法:关键字平方之后取中间几位数作为散列地址
折叠法:分割求和
除留余数法:对于散列表长为m的散列函数公式为:f(key)=key mod p (p<=m),通常p为小于或等于表长的最小质数或不包含20质因子的合数
随机数法:同一个记录存储和查找时用同一个随机因子
处理散列冲突的方法
开放定址法
再散列函数法:多个散列函数,一个冲突了用另一个
链地址法:将所有关键字为同义词的记录存储在一个单链表中
公共溢出区法:有冲突的存到另一个散列表中
散列表查找实现
public class HashTable {
int hashsize = 0;//哈希表的大小
int count = 0;//哈希表中元素个数
int[] hash;//定义一个数组来作为哈希表
HashTable() {
}
/*构造一个大小为hashsize的哈希表*/
HashTable(int hashsize) {
this.count = hashsize;
this.hashsize = hashsize;
this.hash = new int[hashsize];
for (int i = 0; i < hashsize; i++) {
hash[i] = -32768;
}
}
/*散列函数*/
int Hash(int key) {
return key % this.hashsize;/*除留余数法*/
}
void InsertHash(int key) {
int addr = Hash(key);//求散列地址
while (this.hash[addr] != -32768) {/*如果不为空则冲突*/
addr = (addr + 1) % this.hashsize;/*开放定址法的线性探测*/
}
this.hash[addr] = key;/*找到空位之后将key插入*/
}
/*查询到之后将地址返回*/
int SearchHash(int key) {
int address = Hash(key);
while (this.hash[address] != key) {
address = (address + 1) % this.hashsize;
if (this.hash[address] == -32768 || address == Hash(key))
/*如果循环回到原点*/
return -99;//查询不到
}
return address;
}
}
//测试代码
public static void main(String[] args) {
HashTable table = new HashTable(12);
int[] arr={12,67,56,16,25,37,22,29,15,47,48,34};
for (int i = 0; i < arr.length; i++) {
table.InsertHash(arr[i]);
}
for (int i = 0; i < 12; i++) {
System.out.print(i+" ");
}
System.out.println(" ");
for (int i = 0; i < 12; i++) {
System.out.print(table.hash[i]+" ");
}
System.out.println("************");
for (int i = 0; i < 12; i++) {
if (table.SearchHash(arr[i])!=-99)
System.out.println(arr[i]+" "+table.SearchHash(arr[i]));//查找成功打印地址
else System.out.println(arr[i]+"false");//查找失败,表中无key
}
System.out.println("*******");
System.out.println(table.SearchHash(99));//演示查找不到情况
}
/**
* @return void
* @Description 冒泡排序,每次相邻两个比较;局部小的往前走,一遍之后最大的沉到最后
* 复杂度为O(n^2),空间复杂度O(1),稳定
* @Param 数组arr
*/
static void bubbleSort(int[] arr) {
for (int i = arr.length - 1; i > 0; i--) {
boolean flag = false;//添加标志位 当数据有序时省略不必要的比较
for (int j = 0; j < i; j++) {
if (arr[j] > arr[j + 1]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
flag = true;
}
}
if (!flag) break;
}
}
/**
* @return void
* @Description 简单选择排序,固定一个数,从其后找最小数与其交换,减少了交换次数(有点暴力法的味道)
* 时间复杂度O(n^2),空间复杂度O(1),稳定
* @Param 数组arr
*/
static void selectSort(int[] arr) {
for (int i = 0; i < arr.length - 1; i++) {
int min = i;//指向最小值的指针
for (int j = i + 1; j < arr.length; j++) {
if (arr[j] < arr[min]) {
min = j;
}
}
if (i != min)//如果最小值就在最开始标定的位置,则无需交换
swap(arr, i, min);
}
}
static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
/**
* @return void
* @Description 直接插入排序,假设前面的序列有序,后面每个数进行插入
* 时间复杂度O(n^2),空间复杂度O(1),稳定
* @Param 数组arr
*/
static void insertionSort(int[] arr) {
for (int i = 1; i < arr.length; i++) {
int temp = arr[i];
int j;
for (j = i; j > 0; j--) {//插入到前面有序的数组中
if (arr[j - 1] <= temp) break;//遇到前一个数不大于它时插入前一个数后面
arr[j] = arr[j - 1];
}
arr[j] = temp;
}
}
/**
* @return void
* @Description 希尔排序,其本质就是插入排序,按增量来分割排序的元素
* @Param 数组arr
*/
static void shellSort(int[] arr) {
int increment = arr.length;
while (increment > 1) {
increment = increment / 3 + 1;//选取的增量序列,可替换
for (int i = increment; i < arr.length; i++) {
int temp = arr[i];
int j;
for (j = i; j > i % increment; j -= increment) {//注意j > i % increment是核心点
if (arr[j - increment] <= temp) break;
arr[j] = arr[j - increment];
}
arr[j] = temp;
}
}
}
每个结点的值都大于或等于其左右孩子结点的值,称为大顶堆;或者每个结点的值都小于或等于其左右孩子结点的值,称为小顶堆
排序思路
1.首先将待排序的数组构造成一个大顶堆,此时,整个数组的最大值就是堆结构的顶端
2.将顶端的数与末尾的数交换,此时,末尾的数为最大值,剩余待排序数组个数为n-1
3.将剩余的n-1个数再构造成大顶堆,再将顶端数与n-1位置的数交换,如此反复执行,便能得到有序数组
构建整个堆的时间复杂度为O(n),堆排序的时间 复杂度为O(nlogn);由于构建堆需要比较次数较多,不适合排序序列个数较少情况
public static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
public static void HeapSort(int[] array) {
//建堆过程,i = array.length / 2 - 1为找到最后一个有孩子的父节点
for (int i = array.length / 2 - 1; i >= 0; i--) {
HeapAdjust(array, i, array.length);
}
//排序过程,取大顶堆的堆顶与数组最后一个元素交换,剩下的重新构造大顶堆
for (int j = array.length - 1; j > 0; j--) {
swap(array, 0, j);
HeapAdjust(array, 0, j);
}
}
public static void HeapAdjust(int[] arr, int first, int length) {
int temp = arr[first];
for (int i = first * 2 + 1; i < length; i = i * 2 + 1) {
if (i + 1 < length && arr[i] < arr[i + 1]) i++;//让i先指向子节点中最大的节点
if (arr[i] > temp) { // 如果发现子节点更大,则进行值的交换
swap(arr, first, i);
// 如果子节点更换了,那么,以子节点为根的子树会不会受到影响呢?
// 所以,循环对子节点所在的树继续进行判断
first = i;
} else break;
}
}
public static void MergeSort(int[] array, int first, int end) {
if (first < end) {//当子序列中只有一个元素时结束递归
int mid = (first + end) / 2;//划分子序列
MergeSort(array, first, mid);//对左侧子序列进行递归排序
MergeSort(array, mid + 1, end);//对右侧子序列进行递归排序
Merge(array, first, mid, end);//合并
}
}
//合并两个有序序列变为一个有序序列
public static void Merge(int[] arr, int left, int mid, int right) {
int[] temp = new int[arr.length];
//设置两个指针放在原数组两半,再设置一个指针给暂存数组
int L = left, R = mid + 1, T = left;
while (L <= mid && R <= right) {
//比较找出最小值放入暂存数组
if (arr[L] <= arr[R]) {
temp[T++] = arr[L++];
} else {
temp[T++] = arr[R++];
}
}
//补全未比较到的数值,当两半不一样长时的情况处理
while (L <= mid) temp[T++] = arr[L++];
while (R <= right) temp[T++] = arr[R++];
for (int i = left; i <= right; i++) {//将暂存数组的值复制回原数组
arr[i] = temp[i];
}
}
public static void QuickSort(int[] array, int first, int end) {
if (first < end) {
int p = Partition(array, first, end);//算出枢轴值
QuickSort(array, first, p - 1);//对低子表递归排序
QuickSort(array, p + 1, end);//对高子表递归排序
}
}
public static int Partition(int[] arr, int low, int high) {
int p;
p = arr[low];//用表的第一个记录作为枢轴记录
//循环,返回第一个记录排序好后应该在的位置
//使用两个指针,比较并进行交换
while (low < high) {
while (low < high && arr[high] >= p) high--;
swap(arr, high, low);
while (low < high && arr[low] <= p) low++;
swap(arr, high, low);
}
return low;
}
public static void swap(int[] arr, int i, int j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}