数据结构与算法基础笔记

170 阅读15分钟

本文为3月份准备面试学习左程云算法笔记记录,在此保存一下文章以便复习记忆。此文只涉及算法基础,更多内容请参考力扣与左程云算法课程。

时间复杂度

一个操作如果和样本的数据量没有关系,每次都是固定的时间内完成的操作,叫做常数操作。

时间复杂度为一个算法流程中,常数操作数量的一个指标。常用O(读作big O)来表示。具体来说,先要对一个算法操作流程非常熟悉,然后去写出这个算法流程中,发生了多少常数操作,进而总结出常数操作数量的表达式。

在表达式中,只要高阶项,不要低阶项,也不要高阶项的系数,剩下的部分如果为f(N),那么时间复杂度为O(f(N))。

评价一个算法的好坏,先看时间复杂度的指标,然后再去分析不同数据样本下的实际运行时间,也就是“常数项时间”。

递归如何计算时间复杂度

master公式

T(N) = a*T(N/B)+O(N^d)

  • log(b,a) > d -----> 复杂度为O(N^log(b,a))
  • log(b,a) = d -----> 复杂度为O(N^d * logN)
  • log(b,a) < d -----> 复杂度为O(N^d)

a是调用几次递归函数,b是等量分解的分数,O(N^d)是除去递归额外的复杂度。

排序算法

异或

交换数组中两个位置的数

/**
 * 交换数组中i和j位置上的两个数
 *
 * @param arr
 * @param i
 * @param j
 */
public static void swap(int[] arr, int i, int j) {
    int temp;
    temp = arr[i];
    arr[i] = arr[j];
    arr[j] = temp;
    /*     异或交换数组两个位置的数
    if (i == j) {
        throw new RuntimeException("交换的位置i和j不能相等");
    }
    arr[i] = arr[i] ^ arr[j];
    arr[j] = arr[i] ^ arr[j];
    arr[i] = arr[i] ^ arr[j];
    */
}

^ 是异或,相同为0,不同为1

冒泡排序

/**
 * 冒泡排序
 *
 * @param 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++) {//每次比较j和j+1的数,把最大(最小)的数移到最右边
            if (arr[j] > arr[j + 1]) {
                swap(arr, j, j + 1);
            }
        }
    }
}

选择排序

/**
 * 选择排序
 *
 * @param arr
 */
public static void selectionSort(int[] arr) {
    for (int i = 0; i < arr.length - 1; i++) {//0~arr.length - 1位置上每次找最大(最小)的数移到i位置上
        for (int j = i + 1; j < arr.length; j++) {
            if (arr[i] > arr[j]) {//升序
                swap(arr, i, j);
            }
        }
    }
}

二分算法(查找最大值)

/**
 * 用二分与递归查找一个数组的最大值
 * @param arr
 * @return
 */
public static int getMax(int[] arr) {
    return process(arr, 0, arr.length - 1);
}

/**
 * 在l~r上查找最大值
 *
 * @param arr
 * @param l
 * @param r
 * @return
 */
private static int process(int[] arr, int l, int r) {
    if (l == r) {
        return arr[l];
    }
    int mid = l + ((r - l) >> 1);
    int leftMax = process(arr, l, mid);
    int rightMax = process(arr, mid + 1, r);
    return Math.max(leftMax, rightMax);
}

归并排序

/**
 * 归并排序
 * @param arr
 */
public static void mergeSort(int[] arr) {
    if (arr == null || arr.length < 2) {
        return;
    }
    mergeSortProcess(arr, 0, arr.length - 1);
}

private static void mergeSortProcess(int[] arr, int l, int r) {
    if (l == r) {
        return;
    }
    int mid = l + ((r - l) >> 1);
    mergeSortProcess(arr, l, mid);
    mergeSortProcess(arr, mid + 1, r);
    merge(arr, l, mid, r);
}

/**
 * 归并排序每一步的merge过程
 * @param arr
 * @param l
 * @param mid
 * @param r
 */
private static void merge(int[] arr, int l, int mid, int r) {
    int[] help = new int[r - l + 1];
    int i = 0;
    /**
     * 左右两个指针分别遍历
     */
    int p1 = l;
    int p2 = mid + 1;
    while (p1 <= mid && p2 <= r) {
        help[i++] = arr[p1] <= arr[p2] ? arr[p1++] : arr[p2++];
    }
    while (p1 <= mid) {
        help[i++] = arr[p1++];
    }
    while (p2 <= r) {
        help[i++] = arr[p2++];
    }
    for (i = 0; i < help.length; i++) {
        arr[l + i] = help[i];
    }
}

快速排序

/**
 * 快速排序
 * @param arr
 */
public static void quickSort(int[] arr) {
    if (arr == null || arr.length < 2) {
        return;
    }
    quickSortProcess(arr, 0, arr.length - 1);
}

public static void quickSortProcess(int[] arr, int L, int R) {
    if (L < R) {
        swap(arr, (int) (L + Math.random() * (R - L + 1)), R);//随机选取一个数作为划分值
        int[] p = partition(arr, L, R);
        quickSortProcess(arr, L, p[0] - 1);
        quickSortProcess(arr, p[1] + 1, R);
    }
}

/**
 * 快速排序的分步过程
 * @param arr
 * @param L
 * @param R
 * @return
 */
public static int[] partition(int[] arr, int L, int R) {
    int less = L - 1;
    int more = R;
    while (L < more) {
        if (arr[L] < arr[R]) {//小于R上的数左移
            swap(arr, ++less, L++);
        } else if (arr[L] > arr[R]) {//大于R上的数右移
            swap(arr, --more, L);
        } else {//等于R上的数指针继续移动
            L++;
        }
    }
    swap(arr, more, R);//最后再把R上的数换到中间去
    return new int[]{less + 1, more};
}

堆排序

public void sort(int[] arr) {
    if (arr == null || arr.length < 2) {
        return;
    }
    //这种构建大根堆略慢
    //for (int i = 0; i < arr.length; i++) {//先将所有数添加进大根堆
    //    heapInsert(arr, i);//O(logN)
    //}
    //这种构建大根堆会快一些
    for (int i = arr.length / 2 - 1; i >= 0; i--) {//先将所有数添加进大根堆
        heapIfy(arr, i, arr.length);
    }
    int heapSize = arr.length;
    swap(arr, 0, --heapSize);
    while (heapSize > 0) {//再对大根堆每次删除最大值节点
        heapIfy(arr, 0, heapSize);//O(logN)
        swap(arr, 0, --heapSize);//将最大值节点放到后面
    }
}

/**
 * 将堆上index位置的数上移
 *
 * @param arr
 * @param index
 */
private void heapInsert(int[] arr, int index) {
    while (arr[index] > arr[(index - 1) / 2]) {
        swap(arr, index, (index - 1) / 2);
        index = (index - 1) / 2;
    }
}

/**
 * 将堆上index位置的数下移
 *
 * @param arr
 * @param index
 * @param heapSize
 */
private void heapIfy(int[] arr, int index, int heapSize) {
    int left = index * 2 + 1;//左孩子下标
    while (left < heapSize) {
        //两个孩子相比
        int largest = left + 1 < heapSize && arr[left + 1] > arr[left] ? left + 1 : left;
        //父亲结点跟大的孩子节点相比
        largest = arr[largest] > arr[index] ? largest : index;
        if (largest == index) {//说明当前节点已经是最大了
            break;
        }
        swap(arr, largest, index);
        index = largest;
        left = index * 2 + 1;	
    }
}

桶排序

public void sort(int[] arr) {
    if (arr==null||arr.length<2){
        return;
    }
    radixSort(arr,0,arr.length-1,maxBits(arr));
}

/**
 * 桶排序
 * @param arr
 * @param L
 * @param R
 * @param digit
 */
private void radixSort(int[] arr, int L, int R, int digit) {
    final int radix = 10;
    int i, j;
    //辅助空间
    int[] bucket = new int[R - L + 1];
    for (int d = 1; d <= digit; d++) {//有多少位就进出桶多少次
        //10个桶
        //count[0] 当前d位是0的数字有多少个
        //count[1] 当前d位是0和1的数字有多少个
        //....
        int[] count = new int[radix];
        for (i = L; i <= R; i++) {//将d为上的每个数计数
            j = getDigit(arr[i], d);
            count[j]++;
        }
        for (i = 1; i < radix; i++) {//计算前缀和
            count[i] = count[i] + count[i - 1];
        }
        for (i = R; i >= L; i--) {//出桶
            j = getDigit(arr[i], d);
            bucket[count[j] - 1] = arr[i];
            count[j]--;
        }
        for (i = L, j = 0; i <= R; i++, j++) {//将辅助空间复制给原始数组
            arr[i] = bucket[j];
        }
    }
}

/**
 * 获取一个数是几位数
 *
 * @param x
 * @param d
 * @return
 */
private int getDigit(int x, int d) {
    return ((x / ((int) Math.pow(10, d - 1))) % 10);
}

/**
 * 获取数组上最长的位数
 * @param arr
 * @return
 */
private int maxBits(int[] arr){
    int max = Integer.MIN_VALUE;
    for (int i=0;i<arr.length;i++){
        max = Math.max(max,arr[i]);
    }
    int res=0;
    while (max!=0){
        res++;
        max/=10;
    }
    return res;
}

排序算法总结

image-20230720213520344.png

链表

回文链表

判断一个链表是否为回文链表

方法一:用额外辅助空间栈,快慢指针法将链表后半部分压入栈。再从头遍历链表,同时弹出栈顶元素,都一样即为回文链表

方法二:不用辅助空间,快慢指针法将后半部分链表反转,两个指针分别从头和尾遍历,都一样为回文链表,最后再恢复反转的链表。

划分链表

将一个链表以某一个值做划分,小于的放左边,等于的放中间,大于的放右边。

image.png code:

public static Node partition(Node head, int pivot) {
    Node sH = null; // 小于部分结点的头
    Node sT = null; // 小于部分结点的尾
    Node eH = null; // 等于部分结点的头
    Node eT = null; // 等于部分结点的尾
    Node mH = null; // 大于部分节点的头
    Node mT = null; // 大于部分结点的尾
    Node next; // 保存下一个结点
    while (head != null) {
        next = head.next;
        head.next = null;
        if (head.val < pivot) {//连接小端
            if (sH == null) {
                sH = head;
            } else {
                sT.next = head;
            }
            sT = head;
        } else if (head.val == pivot) {//连接中端
            if (eH == null) {
                eH = head;
            } else {
                eT.next = head;
            }
            eT = head;
        } else {//连接大端
            if (mH == null) {
                mH = head;
            } else {
                mT.next = head;
            }
            mT = head;
        }
        head = next;
    }
    //小尾连中头,中尾连大头
    if (sT != null) {
        sT.next = eH;
        eT = eT == null ? sT : eT;
    }
    if (eT != null) {
        eT.next = mH;
    }
    return sH != null ? sH : (eH != null ? eH : mH);
}

复制链表

剑指 Offer 35. 复杂链表的复制 - 力扣(LeetCode)

环形链表

找到环形链表第一个入环结点

public static Node getLoopNode(Node head) {
    if (head == null || head.next == null || head.next.next == null) {//少于三个节点不会成环
        return null;
    }
    Node slow = head.next;
    Node fast = head.next.next;
    while (fast!=slow){//快慢指针相遇代表有环
        if (fast.next==null||fast.next.next==null){
            return null;
        }
        fast=fast.next.next;
        slow=slow.next;
    }
    fast=head;
    while (fast!=slow){//再次相遇即为入环结点
        fast=fast.next;
        slow=slow.next;
    }
    return slow;
}

找两个链表的公共部分

剑指 Offer 52. 两个链表的第一个公共节点 - 力扣(LeetCode)

二叉树

遍历

递归

public static void printTree(Node head) {
    if (head == null) {
        return;
    }
    // 先序遍历
    System.out.println("先序遍历:"+head.val);
    printTree(head.left);
    // 中序遍历
    //System.out.println("中序遍历:"+head.val);
    printTree(head.right);
    // 后序遍历
    //System.out.println("后序遍历:"+head.val);
}

迭代

public static void printTreePre(Node head) {
    Deque<Node> stack = new ArrayDeque<>();
    stack.push(head);
    //先序遍历,将头结点入栈,每次弹出时,将右结点先压入,再压左结点
    //周而复始,每次弹出的值就是先序遍历
    while (!stack.isEmpty()){
        Node pop = stack.pop();
        System.out.println("先序遍历:"+pop.val);
        if (pop.right!=null){
            stack.push(pop.right);
        }
        if (pop.left!=null){
            stack.push(pop.left);
        }
    }
}

public static void printTreeLast(Node head){
    Deque<Node> stack = new ArrayDeque<>();
    Deque<Node> collect = new ArrayDeque<>();
    stack.push(head);
    //后序遍历,先序遍历是头左右,将压栈顺序改为先压左节点,再压右节点就是头右左的顺序
    //再反转就是左右头,即为后序遍历
    while (!stack.isEmpty()){
        Node pop = stack.pop();
        collect.push(pop);
        if (pop.left!=null){
            stack.push(pop.left);
        }
        if (pop.right!=null){
            stack.push(pop.right);
        }
    }
    while (!collect.isEmpty()){
        System.out.println("后序遍历:"+collect.pop().val);
    }
}

public static void printTreeIn(Node head){
    Deque<Node> stack = new ArrayDeque<>();
    // 中序遍历,先将所有的左结点入栈,再依次弹出,每次弹出如果有右节点就再将右节点上的左节点入栈
    // 每次弹出的值就是中序遍历
    while (!stack.isEmpty()||head!=null){
        if (head!=null){
            stack.push(head);
            head=head.left;
        }else {
            head=stack.pop();
            System.out.println("中序遍历:"+head.val);
            head=head.right;
        }
    }
}

由前序遍历,中序遍历推导二叉树

思路:前序遍历结构:根节点 | 左节点 | 右结点

中序遍历:左节点 | 根节点 | 右节点

代码:

class Solution {
    HashMap<Integer, Integer> map = new HashMap<>();//标记中序遍历
    int[] preorder;//保留的先序遍历,方便递归时依据索引查看先序遍历的值

    public TreeNode buildTree(int[] preorder, int[] inorder) {
        this.preorder = preorder;
        //将中序遍历的值及索引放在map中,方便递归时获取左子树与右子树的数量及其根的索引
        for (int i = 0; i < inorder.length; i++) {
            map.put(inorder[i], i);
        }
        //三个索引分别为
        //当前根的的索引
        //递归树的左边界,即数组左边界
        //递归树的右边界,即数组右边界
        return recur(0,0,inorder.length-1);
    }

    TreeNode recur(int pre_root, int in_left, int in_right){
        if(in_left > in_right) return null;// 相等的话就是自己
        TreeNode root = new TreeNode(preorder[pre_root]);//获取root节点
        int idx = map.get(preorder[pre_root]);//获取在中序遍历中根节点所在索引,以方便获取左子树的数量
        //左子树的根的索引为先序中的根节点+1 
        //递归左子树的左边界为原来的中序in_left
        //递归左子树的右边界为中序中的根节点索引-1
        root.left = recur(pre_root+1, in_left, idx-1);
        //右子树的根的索引为先序中的 当前根位置 + 左子树的数量 + 1
        //递归右子树的左边界为中序中当前根节点+1
        //递归右子树的右边界为中序中原来右子树的边界
        root.right = recur(pre_root + (idx - in_left) + 1, idx+1, in_right);
        return root;

    }

}

广度优先遍历

public static void getNodeByWidth(Node head) {
    Queue<Node> queue = new LinkedList<>();
    queue.add(head);
    while (!queue.isEmpty()) {//先压左再压右,保证每层从左往右遍历
        Node poll = queue.poll();
        if (poll.left != null) {
            queue.add(poll.left);
        }
        if (poll.right != null) {
            queue.add(poll.right);
        }
    }
}

每层遍历

public static void getNodeByWidthLevel(Node head) {
    Queue<Node> queue = new LinkedList<>();
    Map<Node,Integer> map = new HashMap<>();
    map.put(head,1);
    queue.add(head);
    while (!queue.isEmpty()) {//先压左再压右,保证每层从左往右遍历
        int size = queue.size();//每层节点数量
        for (int i=0;i<size;i++){//这里的for就是对每一层进行遍历,并将下一层进入队列
            Node poll = queue.poll();
            if (poll.left != null) {
                queue.add(poll.left);
                map.put(poll.left,map.get(poll)+1);
            }
            if (poll.right != null) {
                queue.add(poll.right);
                map.put(poll.right,map.get(poll)+1);
            }
        }
    }
}

搜索二叉树检查

中序遍历二叉树,一直保持递增就为搜索二叉树,否则不是。

public static boolean checkBst(Node head,int preValue){
    if (head==null){
        return true;
    }
    boolean left = checkBst(head.left, preValue);
    if (!left){
        return false;
    }
    if (preValue>=head.val){//中序遍历检查值是否递增
        return false;
    }else {
        preValue = head.val;
    }
    return checkBst(head.right,preValue);
}

判断是否是完全二叉树

层序遍历,满足两个条件:1.任一节点不能只有右孩子没有左孩子 2.遇到第一个左右孩子不全的节点,后续所有节点不能有孩子节点。

判断是否是满二叉树

遍历获取二叉树的深度H和总结点个数N。满足2^H-1=N即为满二叉树。

判断是否是平衡二叉树

定义:左右两子树高度相差不能大于1,并且左右子树也满足平衡二叉树定义。

static class ReturnType{
    boolean isBalance;
    int height;
    ReturnType(boolean first,int second){
        isBalance = first;
        height = second;
    }
}

public ReturnType checkBalance(Node head) {
    if (head==null){
        return new ReturnType(true, 0);
    }
    ReturnType left = checkBalance(head.left);
    ReturnType right = checkBalance(head.right);
    int height = Math.max(left.height, right.height) + 1;//此节点的高度
    boolean isBalance = left.isBalance&& right.isBalance&&(Math.abs(left.height)- right.height)<2;
    return new ReturnType(isBalance, height);
}

树形DP

特性:左右子树标准要一致。例如搜索二叉树,满二叉树,平衡二叉树

将整体转化为一个结点的特性。例:判断是否是搜索二叉树,递归调用,在每个节点判断左右子树是否满足搜索二叉树。判断是否是满二叉树,递归后续遍历,判断左右子树是否满足满二叉树..........

比如满二叉树的判断:

image.png

求最低公共祖先

树形dp法

/**
 * 求两个节点的最低公共祖先
 */
public static Node getLowestAncestor(Node head, Node o1, Node o2) {
    //找到o1或者o2返回,没找到返回空值
    if (head == null || head == o1 || head == o2) {
        return head;
    }
    Node left = getLowestAncestor(head.left, o1, o2);
    Node right = getLowestAncestor(head.right, o1, o2);
    //左右都不为空说明找到了o1和o2,返回当前节点
    if (left != null && right != null) {
        return head;
    }
    //返回o1或者o2
    return left != null ? left : right;
}

还有一种是用map记录每一个节点的父节点,然后通过map一直查找o1的父节点并存到set里面,直到head,同样的方式遍历o2的父节点,将o2的父节点加入set里面,当添加失败时就找到了他们的最低公共祖先。

n次折叠纸条,打印折痕方向

二叉数中序遍历

image.png

image.png

前缀树

一个字符串类型的数组arr1,另一个字符串类型的数组arr2。arr2中有哪些字符,是arr1中出现的?请打印。arr2有哪些字符,是作为arr1中某个字符串前缀出现的?请打印。arr2中有哪些字符,是作为arr1中某个字符串前缀出现的?请打印arr2中出现次数最大的前缀。

节点代码:

public static class TrieNode {  
  
    //有多少点经过这里  
    public int path;  
    //有多少点依此为终点  
    public int end;  
    public TrieNode[] nexts;  
  
    public TrieNode() {  
        path = 0;  
        end = 0;  
  		//26代表底下可能可以连26个字母 如nexts[0]!=null 该字母有通向a的路  
        nexts = new TrieNode[26];  
    }  
}

当有字母经过该点时,path++,如果是尾节点,end++;

e等于零表示不存在这样的字符串。例如若想知道有多少字符串以ab为前缀,可以看ab结点上的p值

构建前缀树:

public void insert(String word) {  
    if (word == null) {  
        return;  
    }  
    char[] chs = word.toCharArray();  
    TrieNode node = root;  
    int index = 0;  
    for (int i = 0; i < chs.length; i++) {  
        index = chs[i] - 'a';  
        if (node.nexts[index] == null) {  
            node.nexts[index] = new TrieNode();  
        }  
        node = node.nexts[index];  
        node.path++;  
    }  
    node.end++;  
}

查找指定字符串:

public int search(String word) {  
    if (word == null) {  
        return 0;  
    }  
    char[] chs = word.toCharArray();  
    TrieNode node = root;  
    int index = 0;  
    for (int i = 0; i < chs.length; i++) {  
        index = chs[i] - 'a';  
        if (node.nexts[index] == null) {  
            return 0;  
        }  
        node = node.nexts[index];  
    }  
    return node.end;  
}

查找有多少个字符以指定字符串为前缀:

public int prefixNumber(String pre) {  
    if (pre == null) {  
        return 0;  
    }  
    char[] chs = pre.toCharArray();  
    TrieNode node = root;  
    int index = 0;  
    for (int i = 0; i < chs.length; i++) {  
        index = chs[i] - 'a';  
        if (node.nexts[index] == null) {  
            return 0;  
        }  
        node = node.nexts[index];  
    }  
    return node.path;  
}

贪心算法

大根堆、小根堆的应用。

哈夫曼树构造。

在数据流中,随时取得中位数。

  1. 将第一个数字放进大根堆

  2. 判断第二个数是否小于大根堆堆顶,若是,入大根堆,否则进小根堆

  3. 判断大小根堆的大小,如果大size-小size>=2,较大堆顶弹出进较小的堆 如以下的流程

  4. 5先进入大根堆,3小于5进大根堆,此时大根堆的长度超过小根堆的长度超过2,将5弹出并放进小根堆

  5. 7和4都大于3放进小根堆,此时小根堆的长度超过大根堆的长度超过2,将4弹出并放进大根堆 此时较小的中位数在大根堆中,较大的中位数在小根堆中 整个流程的所有调整都为logN水平

N皇后问题

public static int num1(int n) {  
    if (n < 1) {  
        return 0;  
    }  
    int[] record = new int[n];  
    return process1(0, record, n);  
}  
// 目前来到了第i行,record[0...i-1]之前的行放过的皇后 整体一共有n行 返回值为合理的摆法  
public static int process1(int i, int[] record, int n) {  
    //终止行  
    if (i == n) {  
        return 1;  
    }  
    int res = 0;  
    for (int j = 0; j < n; j++) {  
        if (isValid(record, i, j)) {  
            record[i] = j;  
            res += process1(i + 1, record, n);  
        }  
    }  
    return res;  
}  
  
public static boolean isValid(int[] record, int i, int j) {  
    for (int k = 0; k < i; k++) {  
        // Math.abs(record[k] - j) == Math.abs(i - k)判断两点是否共斜线 即45度  
        if (j == record[k] || Math.abs(record[k] - j) == Math.abs(i - k)) {  
            return false;  
        }  
    }  
    return true;  
}

递归相关

汉诺塔问题:

public static void hanoi(int n) {  
    if (n > 0) {  
        func(n, n, "left", "mid", "right");  
    }  
}  
  
public static void func(int rest, int down, String from, String help, String to) {  
    if (rest == 1) {  
        System.out.println("move " + down + " from " + from + " to " + to);  
    } else {  
        func(rest - 1, down - 1, from, to, help);  
        func(1, down, from, help, to);  
        func(rest - 1, down - 1, help, from, to);  
    }  
}  
  
public static void main(String[] args) {  
    int n = 3;  
    hanoi(n);  
}

打印字符串子序列

image-20240303230303840

public static void process(char[] chs, int i, List<Character> res) {  
    if(i == chs.length) {//一条分支走完  
        printList(res);
        return;
    }  
    List<Character> resKeep = copyList(res);  
    resKeep.add(chs[i]);  
    process(chs, i+1, resKeep);//左走  
    List<Character> resNoInclude = copyList(res);  
    process(chs, i+1, resNoInclude);//右走  
}

打印符串全排列

public static void process(char[] chs, int i, ArrayList<String> res) {  
    if (i == chs.length) {//排列完一次  
        res.add(String.valueOf(chs));  
    }  
    boolean[] visit = new boolean[26];  
    for (int j = i; j < chs.length; j++) {      
        swap(chs, i, j);  
         process(chs, i + 1, res);  
         swap(chs, i, j);  
    }  
}

不重复

public static void process(char[] chs, int i, ArrayList<String> res) {  
    if (i == chs.length) {  
        res.add(String.valueOf(chs));  
    }  
    boolean[] visit = new boolean[26];  
    for (int j = i; j < chs.length; j++) {  
        if (!visit[chs[j] - 'a']) {  
            visit[chs[j] - 'a'] = true;  
            swap(chs, i, j);  
            process(chs, i + 1, res);  
            swap(chs, i, j);  
        }  
    }  
}