“这是我参与更文挑战的第21天,活动详情查看: 更文挑战”
上一篇是数据结构与算法新手入门第六篇,本篇是数据结构与算法新手入门第七篇,快来体验一下算法的魅力吧!
认识异或运算
异或运算:相同为0,不同为1。异或运算可以理解为无进位相加!
基本用法
例子1
int a = 8;
int b = 2;
// 1000
// 0010
// 1010 = 10
System.out.println(a ^ b);
例子2
无进位相加可以理解为1+1=2,但是不进位,所以遇到满二进一,不进位,认为等于0即可。
int a = 11;
int b = 3;
// 1011
// 0011
// 1000 = 8
System.out.println(a ^ b);
异或运算的性质
- 0 ^ N == N
- N ^ N == N
- 异或运算满足交换律和结合率
上面的性质用无进位相加来理解就非常的容易
题目一
如何不用额外变量交换两个数
// 如何不用额外变量交换两个数
public static void main(String[] args) {
int a = 10;
int b = 20;
a = a ^ b;
b = a ^ b;
a = a ^ b;
System.out.println("a=" + a);
System.out.println("b=" + b);
}
题目二
一个数组中有一种数出现了奇数次,其他数都出现了偶数次,怎么找到并打印这种数。
思路:异或满足交换律,也就是跟位置无关,其次偶数次的数异或必为0,奇数次的数异或必为那个数。
public static void printOddTimesNum(int[] arr) {
int eor = 0;
for (int i = 0; i < arr.length; i++) {
eor ^= arr[i];
}
System.out.println(eor);
}
题目三
怎么把一个int类型的数,提取出最右侧的1来?而不是采用遍历求解。
思路:先对这个数取反,然后加1,在&
rightOne = a & (-a)
一个数的反数等价于(~a) + 1,所以rightOne = a & ((~a) + 1)
int a = 6;
// 0110
// 取反
// 1001
// 加1
// 1010
// 0110
// &
// 1010
// 0010
题目四
给定一个int类型的数,统计二进制位上有多少个1?
思路:先求出最右侧的1,然后把最右侧的1去掉,依次再求出最右侧的1,再去掉最右侧的1。
// 求一个数的二进制位上有多少个1
public static int bit1Count(int num) {
int count = 0;
while (num != 0) {
int rightOne = num & -num; // num & ((~num) + 1)
count++;
num ^= rightOne;// 每次去掉最右侧的1
}
return count;
}
题目五
一个数组中有两种数出现了奇数次,其他数都出现了偶数次,怎么找到并打印这两种数?
思路:出现偶数次的数异或后必为0,假设出现奇数次的两个数分别为a、b,如果数组中每个数都异或下,最后的结果必然是a ^ b,必然退出a!=b,如果a等于b,则a^b == 0了,所以a不等于b,必然在某个二进制位上a和b位置上必然为0或1,假设第二位上,a的第二位上为0,则b的第二位上则为1,或者a的第二位上为1,则b的第二位上则为0,只会出现这两种情况。假设数组中所有数的第二位上都异或下,必然所有数的第二位上的结果不是0就是1,假设a的第二位上是1,则b的第二位上0,则a^b^其他所有数的结果不等于0的,必然是a,则b等于a^b^a。是不是很晕,看下面的代码吧!
// arr中,有两种数,出现奇数次
public static void printOddTimesNum(int[] arr) {
int eor = 0;
for (int i = 0; i < arr.length; i++) {
eor ^= arr[i];
}
// eor = a^b
// eor != 0
// eor某位置上必然存在1,把最右侧的1提取出来
int rightOne = eor & (-eor);
int onlyOne = 0;
for (int i = 0; i < arr.length; i++) {
if ((arr[i] & rightOne) != 0) {
onlyOne ^= arr[i];
}
}
System.out.println(onlyOne + " " + (onlyOne ^ eor));
}
链表
单链表中把给定的值都删掉
思路:分两种情况,第一种情况删掉的节点正好是头部。这种情况直接把头部指针移动到不需要删除的位置上即可。第二种情况,要删除的节点在链表中间,因为是单链表,单链表的只有next,没有pre,所以需要用一个指针记录当前节点的前一个节点。
在双链表中把指定的值都删掉,思路类似,仅仅多了一个指针而已。
public class Code02_DeleteGivenValue {
public static class Node {
int val;
Node next;
public Node(int data) {
this.val = data;
}
}
// 把给定的值都删除
public static Node deleteAllGivenValue(Node head, int val) {
// head来到第一个不需要删的位置
while (head != null) {
if (head.val != val) {
break;
}
head = head.next;
}
Node pre = head;
Node cur = head;
while (cur != null) {
if (cur.val == val) {
pre.next = cur.next;
} else {
pre = cur;
}
cur = cur.next;
}
return head;
}
public static void main(String[] args) {
// 1 2 1 1 3 2
Node node1 = new Node(1);
Node node2 = new Node(2);
node1.next = node2;
Node node11 = new Node(1);
node2.next = node11;
Node node111 = new Node(1);
node11.next = node111;
Node node3 = new Node(3);
node111.next = node3;
node3.next = new Node(2);
Node head = deleteAllGivenValue(node1, 1);
while (head != null) {
System.out.print(head.val + " ");
head = head.next;
}
System.out.println();
}
}
队列
队列的特性:先进先出,好似排队。
单链表实现队列
// 单项链表实现队列 尾部添加、头部取出,即满足先进先出
public static class Node<T> {
T val;
Node<T> next;
public Node(T val) {
this.val = val;
}
}
public static class MyQueue<T> {
Node<T> head;
Node<T> tail;
int size;
public boolean isEmpty() {
return size == 0;
}
public void offer(T data) {
Node cur = new Node(data);
if (tail == null) {
head = tail = cur;
} else {
tail.next = cur;
tail = cur;
}
size++;
}
public T poll() {
T ans = null;
if (head != null) {
ans = head.val;
head = head.next;
size--;
} else {
tail = null;
}
return ans;
}
public T peek() {
if (head == null) {
return null;
}
return head.val;
}
}
双链表实现队列
什么是双端队列?
双端队列:头部即可添加也可取出,尾部即可添加也可取出。
队列满足先进先出,即头部添加,尾部取出。或者尾部添加,头部取出都满足队列特性。
public static class DoubleNode<T> {
T val;
DoubleNode<T> pre;
DoubleNode<T> next;
public DoubleNode(T data) {
this.val = data;
}
}
// 队列
public static class MyQueue<T> {
MyDeque<T> deque;
public MyQueue() {
this.deque = new MyDeque<>();
}
// 添加
public void offer(T data) {
deque.offerFromHead(data);
}
// 取出
public T poll() {
return deque.pollFromTail();
}
}
// 双链表实现双端队列(头部既可以添加也可以取出,尾部既可以添加也可以取出)
public static class MyDeque<T> {
DoubleNode<T> head;
DoubleNode<T> tail;
int size;
public boolean isEmpty() {
return size == 0;
}
// 头部添加
public void offerFromHead(T data) {
DoubleNode cur = new DoubleNode(data);
if (head == null) {
head = tail = cur;
} else {
cur.next = head;
head.pre = cur;
head = cur;
}
size++;
}
// 头部取出
public T pollFromHead() {
T ans = null;
if (head == null) {
return ans;
}
size--;
ans = head.val;
if (head == tail) {
head = tail = null;
} else {
head = head.next;
head.pre = null;
}
return ans;
}
// 查看头部
public T peekHead() {
return head != null ? head.val : null;
}
// 尾部添加
public void offerFromTail(T data) {
DoubleNode cur = new DoubleNode(data);
if (head == null) {
head = tail = cur;
} else {
tail.next = cur;
cur.pre = tail;
tail = cur;
}
size++;
}
// 尾部取出
public T pollFromTail() {
T ans = null;
if (head == null) {
return ans;
}
size--;
ans = tail.val;
if (head == tail) {
head = tail = null;
} else {
tail = tail.pre;
tail.next = null;
}
return ans;
}
// 查看尾部
public T peekTail() {
return tail != null ? tail.val : null;
}
}
数组实现队列
数组的特性,一旦初始化,数组的大小就固定了,可以使用技巧让数组可以重复利用,即环形数组。
// 数组实现队列 先进先出(尾部添加、头部取出)
public static class MyQueue<T> {
private Object[] arr;
private int offerIndex;
private int pollIndex;
private int size;
private final int limit;
public MyQueue(int limit) {
this.arr = new Object[limit];
this.offerIndex = 0;
this.pollIndex = 0;
this.size = 0;
this.limit = limit;
}
public boolean isFull() {
return size == limit;
}
public boolean isEmpty() {
return size == 0;
}
// 入队
public void offer(Object data) {
if (isFull()) {
throw new RuntimeException("队列已满,不能再添加了!");
}
size++;
arr[offerIndex] = data;
offerIndex = nextIndex(offerIndex);
}
// 出队
public T poll() {
if (isEmpty()) {
throw new RuntimeException("队列已空,不能再取出了!");
}
size--;
T ans = (T) arr[pollIndex];
pollIndex = nextIndex(pollIndex);
return ans;
}
// 如果现在的下标是i,返回下一个位置
private int nextIndex(int i) {
return i < limit - 1 ? i + 1 : 0;
}
}
如何用队列结构实现栈结构
思路:栈的特性先进后出或后进先出,可以使用两个队列,入栈用queue正常放,出栈的话,需要help辅助队列记录剩余的数。然后交换位置。
// 用队列结构实现栈结构
public static class MyStack<T> {
private Queue<T> queue;
private Queue<T> help;
public MyStack() {
this.queue = new LinkedList<>();
this.help = new LinkedList<>();
}
// 入栈
public void push(T val) {
queue.offer(val);
}
// 出栈
public T pop() {
while (queue.size() > 1) {
help.offer(queue.poll());
}
T ans = queue.poll();
Queue<T> tmp = queue;
queue = help;
help = tmp;
return ans;
}
public T peek() {
while (queue.size() > 1) {
help.offer(queue.poll());
}
T ans = queue.poll();
help.offer(ans);
Queue<T> tmp = queue;
queue = help;
help = tmp;
return ans;
}
}
栈
栈的特性:先进后出,犹如弹匣。
单链表实现栈
// 单向链表实现栈,先进后出,后进先出,头部添加、头部移除即可实现栈
public static class Node<T> {
T val;
Node<T> next;
public Node(T val) {
this.val = val;
}
}
public static class MyStack<T> {
Node<T> head;
int size;
public boolean isEmpty() {
return size == 0;
}
public void push(T data) {
Node cur = new Node(data);
if (head != null) {
cur.next = head;
}
head = cur;
size++;
}
public T pop() {
T ans = null;
if (head != null) {
ans = head.val;
head = head.next;
size--;
}
return ans;
}
}
双链表实现栈
栈:先进后出或者后进先出。
双端队列实现栈:尾部添加、尾部取出。或者头部添加、头部取出都满足栈特性。
public static class DoubleNode<T> {
T val;
DoubleNode<T> pre;
DoubleNode<T> next;
public DoubleNode(T data) {
this.val = data;
}
}
// 栈
public static class MyStack<T> {
MyDeque<T> deque;
public MyStack() {
this.deque = new MyDeque<>();
}
// 入栈
public void push(T data) {
deque.offerFromTail(data);
}
// 出栈
public T pop() {
return deque.pollFromTail();
}
}
// 双链表实现双端队列(头部既可以添加也可以取出,尾部既可以添加也可以取出)
public static class MyDeque<T> {
DoubleNode<T> head;
DoubleNode<T> tail;
int size;
public boolean isEmpty() {
return size == 0;
}
// 头部添加
public void offerFromHead(T data) {
DoubleNode cur = new DoubleNode(data);
if (head == null) {
head = tail = cur;
} else {
cur.next = head;
head.pre = cur;
head = cur;
}
size++;
}
// 头部取出
public T pollFromHead() {
T ans = null;
if (head == null) {
return ans;
}
size--;
ans = head.val;
if (head == tail) {
head = tail = null;
} else {
head = head.next;
head.pre = null;
}
return ans;
}
// 查看头部
public T peekHead() {
return head != null ? head.val : null;
}
// 尾部添加
public void offerFromTail(T data) {
DoubleNode cur = new DoubleNode(data);
if (head == null) {
head = tail = cur;
} else {
tail.next = cur;
cur.pre = tail;
tail = cur;
}
size++;
}
// 尾部取出
public T pollFromTail() {
T ans = null;
if (head == null) {
return ans;
}
size--;
ans = tail.val;
if (head == tail) {
head = tail = null;
} else {
tail = tail.pre;
tail.next = null;
}
return ans;
}
// 查看尾部
public T peekTail() {
return tail != null ? tail.val : null;
}
}
数组实现栈
正常使用即可。
// 数组实现栈 先进后出
public static class MyStack<T> {
private Object[] arr;
private int index = 0;
private int size = 8;
public MyStack(int size) {
this.size = size;
this.arr = new Object[size];
}
public boolean isEmpty() {
return index == 0;
}
public boolean isFull() {
return index == size;
}
// 添加
public void push(Object data) {
if (isFull()) {
throw new RuntimeException("栈满了,不能再添加了~");
}
arr[index++] = data;
}
// 弹出
public T pop() {
if (isEmpty()) {
throw new RuntimeException("栈空了,不能再弹出了~");
}
return (T) arr[--index];
}
}
实现一个特殊的栈,在基本功能的基础上,再实现返回栈中最小元素的功能
- pop、push、getMin操作的时间复杂度都是 O(1)。
- 设计的栈类型可以使用现成的栈结构。
思路:采用两个栈,一个栈正常存放数,另一个栈存栈中最小数。
// 实现一个特殊的栈,在基本功能的基础上,在实现返回栈中的最小元素的功能
public static class MyStack1 {
private Stack<Integer> stackData;
private Stack<Integer> stackMin;
public MyStack1() {
this.stackData = new Stack<>();
this.stackMin = new Stack<>();
}
// 添加
public void push(int newNum) {
if (stackMin.isEmpty()) {
stackMin.push(newNum);
} else if (newNum <= getMin()) {
stackMin.push(newNum);
}
stackData.push(newNum);
}
// 弹出
public int pop() {
if (stackData.isEmpty()) {
throw new RuntimeException("Your stack is Empty.");
}
int val = stackData.pop();
if (val == getMin()) {
stackMin.pop();
}
return val;
}
public int getMin() {
if (stackMin.isEmpty()) {
throw new RuntimeException("Your stack is Empty.");
}
return stackMin.peek();
}
}
public static class MyStack2 {
private Stack<Integer> stackData;
private Stack<Integer> stackMin;
public MyStack2() {
this.stackData = new Stack<>();
this.stackMin = new Stack<>();
}
public void push(int newNum) {
if (stackMin.isEmpty()) {
stackMin.push(newNum);
} else if (newNum < getMin()) {
stackMin.push(newNum);
} else {
int val = getMin();
stackMin.push(val);
}
stackData.push(newNum);
}
public int pop() {
if (stackData.isEmpty()) {
throw new RuntimeException("Your stack is Empty.");
}
stackMin.pop();
return stackData.pop();
}
public int getMin() {
if (stackMin.isEmpty()) {
throw new RuntimeException("Your stack is Empty.");
}
return stackMin.peek();
}
}
如何用栈结构实现队列结构
思路:可以采用两个栈结构,一个存的栈,一个出的栈。
// 用栈结构实现队列结构
public static class MyQueue {
private Stack<Integer> stackPush;
private Stack<Integer> stackPop;
public MyQueue() {
this.stackPush = new Stack<>();
this.stackPop = new Stack<>();
}
// 入队
public void offer(int val) {
stackPush.push(val);
pushToPop();
}
// 出队
public int poll() {
if (stackPush.isEmpty() && stackPop.isEmpty()) {
throw new RuntimeException("Queue is empty!");
}
pushToPop();
return stackPop.pop();
}
// 查看头部
public int peek() {
if (stackPush.isEmpty() && stackPop.isEmpty()) {
throw new RuntimeException("Queue is empty!");
}
pushToPop();
return stackPop.peek();
}
// push栈向pop栈倒入数据
public void pushToPop() {
if (stackPop.isEmpty()) {
while (!stackPush.isEmpty()) {
stackPop.push(stackPush.pop());
}
}
}
}
递归
求数组arr[L...R]中的最大值,怎么用递归方法实现。
public static int getMax(int[] arr, int L, int R) {
if (L == R) {
return arr[L];
}
int mid = L + ((R - L) >> 1);
return Math.max(getMax(arr, L, mid), getMax(arr, mid + 1, R));
}
总结
本篇开始介绍了异或的特性及一些骚操作,紧接着介绍了链表、队列、栈的常见考题,最后介绍了递归的基本使用。
欢迎大家关注公众号(MarkZoe)互相学习、互相交流。