看《我的第一本算法书》有感

链表

链表是数据结构之一,其中的数据呈线性排列。在链表中,数据的添加和删除都较为方便,就是访问比较耗费时间。

在这里插入图片描述
Blue、Yellow、Red 这 3 个字符串作为数据被存储于链表中。每个数据都有 1 个“指针”,它指向下一个数据的内存地址。

package demo2;
public class Node {
	int data;
	Node next;
	public Node(int data) {
		this.data=data;
	}
	//增加节点
	public Node append(Node node) {
		//当前节点
		Node currentNode = this;
		//检查是否成链,并找到最后那个节点
		while(true) {
			Node nextNode = currentNode.next;
			if(nextNode==null) {
				break;
			}
			currentNode = nextNode;
		}
		//新增node节点
		currentNode.next=node;
		return this;
	}
	//插入一个节点做为当前节点的下一个节点
	public void after(Node node) {
		Node nextNext = next;
		this.next=node;
		node.next=nextNext;
	}
	//显示所有节点信息
	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 removeNext() {
		Node newNext = next.next;
		this.next=newNext;
	}
}

package demo2.test;
import demo2.Node;
public class TestNode {
	public static void main(String[] args) {
		//创建节点
		Node n1 = new Node(1);
		Node n2 = new Node(2);
		Node n3 = new Node(3);
		//成链
		n1.append(n2).append(n3).append(new Node(4));
		//显示所有节点内容
		n1.show();
		//删除一个节点
		n1.removeNext();
		//插入一个新节点
		Node node = new Node(5);
		n1.after(node);
	}
}

我们也可以在链表尾部使用指针,并且让它指向链表头部的数据,将链表变成环形。这便是“循环链表”,也叫“环形链表”。
循环链表没有头和尾的概念。想要保存数量固定的最新数据时通常会使用这种链表。
在这里插入图片描述

public class LoopNode {
	int data;
	LoopNode next=this;
	public LoopNode(int data) {
		this.data=data;
	}
	//插入一个节点做为当前节点的下一个节点
	public void after(LoopNode node) {
		LoopNode nextNext = next;
		this.next=node;
		node.next=nextNext;
	}
	//删除下一个节点
	public void removeNext() {
		LoopNode newNext = next.next;
		this.next=newNext;
	}
	//显示
	public void show(){
		LoopNode now=this;
		while(true){
			System.out.println(now.data+" ");
			if(now.next==null){
				break;
			}
			now=now.next;
		}
	}
	public static void main(String[] args) {
		LoopNode n1 = new LoopNode(1);
		LoopNode n2 = new LoopNode(2);
		LoopNode n3 = new LoopNode(3);
		LoopNode n4 = new LoopNode(4);
		//增加节点
		n1.after(n2);
		n2.after(n3);
		n3.after(n4);
		//删除n2
		n1.removeNext();
		n1.show();
	}
}

链表里的每个数据都只有一个指针,但我们可以把指针设定为两个,并且让它们分别指向前后数据,这就是“双向链表”。使用这种链表,不仅可以从前往后,还可以从后往前遍历数据,十分方便。

但是,双向链表存在两个缺点:一是指针数的增加会导致存储空间需求增加;二是添加和删除数据时需要改变更多指针的指向。

在这里插入图片描述

public class DoubleNode {
	DoubleNode pre;
	DoubleNode next;
	int data;
	public DoubleNode(int data) {
		this.data=data;
	}
	//增节点
	public void after(DoubleNode node) {
		DoubleNode nextNext = next;
		this.next=node;
		node.pre=this;
	}
	//正序删
	public void removenext(){
		DoubleNode want=next.next;
		this.next=want; 	
	}
	//反序删
	public void removepre(){
		DoubleNode want=pre.pre;
		this.pre=want;
	}
	//正序遍历
	public void show(){
		DoubleNode now=this;
		while(true){
			System.out.println(now.data+" ");
			if(now.next==null){
				break;
			}
			now=now.next;
		}
	}
	//反序遍历
	public void showback(){
		DoubleNode now=this;
		while(true){
			System.out.println(now.data+" ");
			if(now.pre==null){break;}
			now=now.pre;
		}
	}
	public static void main(String[] args) {
		DoubleNode n1 = new DoubleNode(1);
		DoubleNode n2 = new DoubleNode(2);
		DoubleNode n3 = new DoubleNode(3);
		n1.after(n2);
		n2.after(n3);
		n1.removenext();
		n3.removepre();
		n1.show();
		n3.showback();
	}
}

数组

数组也是数据呈线性排列的一种数据结构。

public class MyArray {
	private int[] elements;
	public MyArray() {
		elements = new int[0];
	}
	//获取数组长度
	public int size() {
		return elements.length;
	}
	// 往数组的末尾添加一个元素
	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;
	}
	//这里我试过改成foreach遍历,然而并不行,这种遍历很粗暴,
	//不像上面的能控制遍历到哪个位置
	// 打印所有元素到控制台
	public void show() {
		System.out.println(Arrays.toString(elements));
	}
	// 删除数组中的元素
	public void delete(int index) {
		if (index < 0 || index > elements.length - 1) {
			throw new RuntimeException("下标越界");
		}
		int[] newArr = new int[elements.length - 1];
		for (int i = 0; i < newArr.length; i++) {
			if (i < index) {
				newArr[i] = elements[i];
			} else {
				newArr[i] = elements[i + 1];
			}
		}
		elements = newArr;
	}
	// 取出指定位置的元素
	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) {
		int[] newArr = new int[elements.length + 1];
		for (int i = 0; i < elements.length; i++) {
			if (i < index) {
				newArr[i] = elements[i];
			} else {
				newArr[i + 1] = elements[i];
			}
		}
		newArr[index] = element;
		elements = newArr;
	}
	// 替换指定位置的元素
	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) {
		MyArray ma = new MyArray();
		System.out.println(ma.size());
		ma.show();
		ma.add(99);
		ma.add(98);
		ma.add(97);
		ma.show();
		ma.delete(1);
		ma.show();
		int element = ma.get(1);
		System.out.println(element);
		System.out.println("=====================");
		ma.add(96);
		ma.add(95);
		ma.add(94);
		ma.show();
		//插入元素到指定位置
		ma.insert(3, 33);
		ma.show();
		System.out.println("=====================");
		//替换指定位置的元素
		ma.set(1, 100);
		ma.show();
		ma.set(-1, 100);
		System.out.println(ma.size());
	}
}

在链表和数组中,数据都是线性地排成一列。在链表中访问数据较为复杂,添加和删除数据较为简单;而在数组中访问数据比较简单,添加和删除数据却比较复杂。

在这里插入图片描述

线性查找&二分法查找

线性查找是一种在数组中查找数据的算法,在数据量大且目标数据靠后,或者目标数据不存在时,比较的次数就会更多,也更为耗时。
二分查找也是一种在数组中查找数据的算法,它只能查找已经排好序的数据。
二分查找与线性查找相比速度上得到了指数倍提高。但是,二分查找必须建立在数据已经排好序的基础上才能使用,因此添加数据时必须加到合适的位置,这就需要额外耗费维护数组的时间。而使用线性查找时,数组中的数据可以是无序的,因此添加数据时也无须顾虑位置,直接把它加在末尾即可,不需要耗费时间。

public class T{
	int[] a;
	public T() {
		a = new int[] {2,3,5,6,8};
	}
    //线性查找
	public int search(int target) {
		//遍历数组
		for(int i=0;i<a.length;i++) {
			if(a[i]==target) {
				return i;
			}
		}
		//没有找到对应的元素
		return -1;
	}
	//二分法查找
	public int binarySearch(int target) {
		int begin = 0;
		int end = a.length-1;
		int mid = (begin+end)/2;
		//循环查找
		while(true) {		
			if(begin>end) {
				return -1;
			}
			if(a[mid]==target) {
				return mid;		
			}else{	
				if(a[mid]>target) {				
					end=mid-1;			
				}else {				
					begin = mid+1;
				}			
				mid=(begin+end)/2;
			}
		}
	}
    public static void main(String[] args) {
		T aa=new T();
		System.out.println(aa.search(5));
		System.out.println(aa.binarySearch(5));
    }
}

栈也是一种数据呈线性排列的数据结构,不过在这种结构中,我们只能访问最新添加的数据。
在这里插入图片描述在这里插入图片描述像栈这种最后添加的数据最先被取出,即“后进先出”的结构,我们称为 Last In First Out,简称 LIFO。
与链表和数组一样,栈的数据也是线性排列,但在栈中,添加和删除数据的操作只能在一端进行,访问数据也只能访问到顶端的数据。想要访问中间的数据时,就必须通过出栈操作将目标数据移到栈顶才行。
栈只能在一端操作这一点看起来似乎十分不便,但在只需要访问最新数据时,使用它就比较方便了。
深度优先搜索算法,通常会选择最新的数据作为候补顶点。在候补顶点的管理上就可以使用栈。

队列

在这里插入图片描述
在这里插入图片描述

像队列这种最先进去的数据最先被取来,即“先进先出”的结构,我们称为 First In First Out,简称 FIFO。
与栈类似,队列中可以操作数据的位置也有一定的限制。在栈中,数据的添加和删除都在同一端进行,而在队列中则分别是在两端进行的。队列也不能直接访问位于中间的数据,必须通过出队操作将目标数据变成首位后才能访问。
广度优先搜索算法,通常就会从搜索候补中选择最早的数据作为下一个顶点。此时,在候补顶点的管理上就可以使用队列。

public class StackQueue {
	int[] elements;
	public StackQueue() {
		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("stack is empty");
		}
		int element = elements[elements.length-1];
		int[] newArr = new int[elements.length-1];
		for(int i=0;i<elements.length-1;i++) {
			newArr[i]=elements[i];
		}
		elements=newArr;
		return element;
	}
	//查看栈顶元素
	public int peek() {
		if(elements.length==0) {
			throw new RuntimeException("stack is empty");
		}
		return elements[elements.length-1];
	}
	//出队
	public int poll() {
		int element = elements[0];
		int[] newArr = new int[elements.length-1];
		for(int i=0;i<newArr.length;i++) {
			newArr[i]=elements[i+1];
		}
		elements=newArr;
		return element;
	}
	//判断是否为空
	public boolean isEmpty() {
		return elements.length==0;
    }
    public static void main(String[] args) {
        StackQueue sq=new StackQueue();
        sq.push(1);
        sq.push(2);
        sq.push(3);
        System.out.println(sq.pop());
        System.out.println(sq.peek());
        System.out.println(sq.poll());
        System.out.println(sq.isEmpty());
    }
}

冒泡排序

在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述交换数字的次数和输入数据的排列顺序有关。假设出现某种极端情况,如输入数据正好以从小到大的顺序排列,那么便不需要任何交换操作;反过来,输入数据要是以从大到小的顺序排列,那么每次比较数字后便都要进行交换。

public void bubbleSort(int[]  arr) {
		for(int i=0;i<arr.length-1;i++) {
			for(int j=arr.length-1;j>i;j--) {
				if(arr[j]<arr[j-1]) {
					int temp=arr[j];
					arr[j]=arr[j-1];
					arr[j-1]=temp;
				}
			}
		}
	}

选择排序

在这里插入图片描述在这里插入图片描述

public 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;
				}
			}
			if(i!=minIndex) {
				int temp=arr[i];
				arr[i]=arr[minIndex];
				arr[minIndex]=temp;
			}
		}
	}

插入排序

在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述
插入排序的思路就是从右侧的未排序区域内取出一个数据,然后将它插入到已排序区域内合适的位置上。

public 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;
			}
		}
	}

归并排序

在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述完成一行归并所需的运行时间取决于这一行的数据量。

public static void mergeSort(int[] arr,int low,int high) {
		int middle=(high+low)/2;
		if(low<high) {
			mergeSort(arr, low, middle);
			mergeSort(arr, middle+1, high);
			merge(arr,low,middle,high);
		}
	}
	public static void merge(int[] arr,int low,int middle, int high){
		int[] temp = new int[high-low+1];
		int i=low;
		int j=middle+1;
		int index=0;
		while(i<=middle&&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<=middle) {
			temp[index]=arr[i];
			i++;
			index++;
		}
		for(int k=0;k<temp.length;k++) {
			arr[k+low]=temp[k];
		}
	}

快速排序

在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述本质上就是[ 比基准值小的数 ] 基准值 [ 比基准值大的数 ]
快速排序是一种“分治法”。它将原本的问题分成两个子问题(比基准值小的数和比基准值大的数),然后再分别解决这两个问题。子问题,也就是子序列完成排序后,再像一开始说明的那样,把他们合并成一个序列,那么对原始序列的排序也就完成了。
归并排序也可看作是一种递归的分治法。

public 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);
		}
	}

哈希表

可以使数据的查询效率得到显著提升

在这里插入图片描述在哈希表中,我们可以利用哈希函数快速访问到数组中的目标数据。如果发生哈希冲突,就使用链表进行存储。这样一来,不管数据量为多少,我们都能够灵活应对。
如果数组的空间太小,使用哈希表的时候就容易发生冲突,线性查找的使用频率也会更高;反过来,如果数组的空间太大,就会出现很多空箱子,造成内存的浪费。因此,给数组设定合适的空间非常重要。

在存储数据的过程中,如果发生冲突,可以利用链表在已有数据的后面插入新数据来解决冲突。这种方法被称为“链地址法”。
“开放地址法”指当冲突发生时,立刻计算出一个候补地址(数组上的位置)并将数据存进去。如果仍然有冲突,便继续计算下一个候补地址,直到有空地址为止。可以通过多次使用哈希函数或“线性探测法”等方法计算候补地址。

哈希函数

哈希函数可以把给定的数据转换成固定长度的无规律数值。转换后的无规律数值可以作为数据摘要应用于各种各样的场景。

在这里插入图片描述在这里插入图片描述在这里插入图片描述
哈希函数的算法中 SHA-2 (Secure Hash Algorithm)是应用较广泛的一个,而 MD5 和 SHA-1 存在安全隐患。不同算法的计算方式也会有所不同,比如 SHA-1 需要经过数百次的加法和移位运算才能生成哈希值。
虽然如果输入的数据相同,那么输出的哈希值也必定相同,但这是在使用同一个算法的前提下得出的结论。

将用户输入的密码保存到服务器时也需要用到哈希函数:
如果把密码直接保存到服务器,可能会被第三者窃听,因此需要算出密码的哈希值,并只存储哈希值。当用户输入密码时,先算出该输入密码的哈希值,再把它和服务器中的哈希值进行比对。这样一来,就算保存的哈希值暴露了,因为哈希函数的第五个特征(输入输出不可逆),第三者也无法得知原本的密码。所以使用哈希函数可以更安全地实现基于密码的用户认证。

现在用代码来设计散列函数:
在这里插入图片描述

public class StuInfo {
	private int age;
	private int count;
	public int getAge() {return age;}
	public void setAge(int age) {this.age = age;}
	public int getCount() {return count;}
	public void setCount(int count) {this.count = count;}
	//散列函数
	public int hashCode() {
		return age;
	}
	public StuInfo(int age, int count) {
		super();
		this.age = age;
		this.count = count;
	}
	public StuInfo(int age) {
		super();
		this.age = age;
	}
	@Override
	public String toString() {
		return "StuInfo [age=" + age + ", count=" + count + "]";
	}
}
public class HashTable {
	private StuInfo[] data = new StuInfo[100];	
	 //向散列表中添加元素
	 public void put(StuInfo stuInfo) {
		//调用散列函数获取存储位置
		int index = stuInfo.hashCode();
		//添加元素
		data[index]=stuInfo;
	}	
	public StuInfo get(StuInfo stuInfo) {
		return data[stuInfo.hashCode()];
	}
	@Override
	public String toString() {
		return "HashTable [data=" + Arrays.toString(data) + "]";
	}
}
public class TestHashTable {
	public static void main(String[] args) {
		StuInfo s1 = new StuInfo(16, 3);
		StuInfo s2 = new StuInfo(17, 11);
		StuInfo s3 = new StuInfo(18, 23);
		StuInfo s4 = new StuInfo(19, 24);
		StuInfo s5 = new StuInfo(20, 9);
		
		HashTable ht = new HashTable();
		ht.put(s1);
		ht.put(s2);
		ht.put(s3);
		ht.put(s4);
		ht.put(s5);
		System.out.println(ht);
		//想要获取的目标数据
		StuInfo target = new StuInfo(18);
		StuInfo info = ht.get(target);
		System.out.println(info);
	}
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值